Skip to content

$search 自动字段集:nameField/name/title 被无条件前置,绕过 SEARCH_AUTO_EXCLUDED_FIELDS——搜索会打到主键 #4483

Description

@os-zhuang

@objectstack/specpackages/spec/src/data/search-fields.ts
发现于:核对 #4419 时(PR #4459),与该 issue 无关,故当时留在 PR 之外
核实基线main @ ec975f1

摘要

autoDefaultFields 逐字段过了三道排除条件来决定哪些字段可以被 $search 自动扫描,
然后把「主标题字段」无条件前置——不再复查那三道条件。于是排除表对这一个字段完全失效。

后果是 $search 会对主键做子串扫描:一个只有 id(text) 和数值列的对象,
ADR-0079 会把它的 nameField 定成 id$search 随即展开成 { id: { $contains: <term> } }

位置

packages/spec/src/data/search-fields.ts:65-82

functionautoDefaultFields(fields,displayField?): string[]{constnames=Object.keys(fields).filter((f)=>{if(SEARCH_AUTO_EXCLUDED_FIELDS.has(f))returnfalse;// ← 66-74 行:三道排除constmeta=fields[f];if(!meta||meta.hidden)returnfalse;constt=meta.type;if(!t)returnfalse;if(SEARCH_AUTO_EXCLUDED_TYPES.has(t))returnfalse;returnSEARCHABLE_TEXTUAL_TYPES.has(t)||SEARCHABLE_ENUM_TYPES.has(t);});// Lead with the display/name field when present.constlead=displayField&&fields[displayField] ? displayField// ← 75-81 行:只查存在
: fields.name ? 'name'
: fields.title ? 'title'
: undefined;if(!lead)returnnames;return[lead, ...names.filter((f)=>f!==lead)];// ← 无条件前置}

lead三条旁路,都只做存在性检查:displayFieldfields.namefields.title
任何一条命中的字段都会进入结果集,无论它是否在排除表里、类型是否可搜、是否 hidden

复现

packages/spec 的构建产物直接调用(实测,非推演):

import{resolveSearchFieldResolution}from'@objectstack/spec/data';constfields={id: {type: 'text'},amount: {type: 'number'}};resolveSearchFieldResolution({ fields });// → { allowed: [], source: 'auto' } ✅ 正确:id 在排除表,amount 类型不符resolveSearchFieldResolution({ fields,displayField: 'id'});// → { allowed: ['id'], source: 'auto' } ❌ 排除表被绕过

这不是构造出来的场景

displayField 的实参是对象的 nameField,而 nameField 由 ADR-0079 的
provisionPrimary(schema, { synthesize: false }) 在注册时自动指定
packages/objectql/src/registry.ts:842):当对象上有可作标题的字段时就指认一个。
对于「唯一文本列就是 id」的表——系统表、连接表、追加型日志表都是这个形状——它指认的就是 id

我在真实注册对象上通过引擎观察到过展开结果,不是只调了这个 helper:

findOne('ledger', { search: 'anything' })
→ 交给 driver 的 AST:{ object: 'ledger', limit: 1,
where: { $or: [{ id: { $contains: 'anything' } }] } }

对象定义是 { id: { type: 'text', primaryKey: true }, amount: { type: 'number' } }

第二层后果:#4254 的 REST 入口闸门也跟着放行

resolveSearchFieldResolution 不只服务引擎侧的自动默认集,它同时是 #4254 那道
REST 入口闸门的判据(packages/metadata-protocol/src/protocol.ts:3751)。
闸门的职责是「拒绝引擎不会扫描的 $searchFields 覆盖」——但因为 id 现在确实在 allowed 里,
一个 $searchFields=id 的请求会被接受,而不是按设计拒绝。

所以这一处偏差同时松动了两层:自动默认集,以及本该收口它的那道闸门。

被打破的是该模块自己写下的不变量

search-fields.ts:42

/** System / audit / heavy fields never auto-included. */exportconstSEARCH_AUTO_EXCLUDED_FIELDS: ReadonlySet<string>=newSet(['id','_id','created','modified','created_at','updated_at','created_by','updated_by','owner_id','organization_id','space','company_id',]);

never 目前不成立。

建议修法

lead 过同一条谓词——它是否可搜,和它是不是主标题无关;lead 的作用应该只是排序
(把主标题排在最前),不该是准入。不通过就退回原 names

consteligible=(f?: string)=>!!f&&names.includes(f);constlead=eligible(displayField) ? displayField
: eligible('name') ? 'name'
: eligible('title') ? 'title'
: undefined;

这样 lead 的排序意图完整保留,只是不再兼作后门。

影响面判断

不是数据泄露,也不属于 #4419 那类「谓词被丢弃」——它产生的是一个更窄且语义错误的谓词
(搜到的是 id 里恰好含该子串的行)。真正的代价是:宣称的不变量没有被执行,
#4254 那道闸门在此之上给出的是一个它自己也不该给的通过。

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions