Repository navigation
$search 自动字段集:nameField/name/title 被无条件前置,绕过 SEARCH_AUTO_EXCLUDED_FIELDS——搜索会打到主键 #4483
Description
包:@objectstack/spec(packages/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:
function autoDefaultFields(fields, displayField?): string[] {
const names = Object.keys(fields).filter((f) => {
if (SEARCH_AUTO_EXCLUDED_FIELDS.has(f)) return false; // ← 66-74 行:三道排除
const meta = fields[f];
if (!meta || meta.hidden) return false;
const t = meta.type;
if (!t) return false;
if (SEARCH_AUTO_EXCLUDED_TYPES.has(t)) return false;
return SEARCHABLE_TEXTUAL_TYPES.has(t) || SEARCHABLE_ENUM_TYPES.has(t);
});
// Lead with the display/name field when present.
const lead = displayField && fields[displayField] ? displayField // ← 75-81 行:只查存在
: fields.name ? 'name'
: fields.title ? 'title'
: undefined;
if (!lead) return names;
return [lead, ...names.filter((f) => f !== lead)]; // ← 无条件前置
}lead 有三条旁路,都只做存在性检查:displayField、fields.name、fields.title。
任何一条命中的字段都会进入结果集,无论它是否在排除表里、类型是否可搜、是否 hidden。
复现
对 packages/spec 的构建产物直接调用(实测,非推演):
import { resolveSearchFieldResolution } from '@objectstack/spec/data';
const fields = { 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. */
export const SEARCH_AUTO_EXCLUDED_FIELDS: ReadonlySet<string> = new Set([
'id', '_id', 'created', 'modified', 'created_at', 'updated_at',
'created_by', 'updated_by', 'owner_id', 'organization_id', 'space', 'company_id',
]);never 目前不成立。
建议修法
把 lead 过同一条谓词——它是否可搜,和它是不是主标题无关;lead 的作用应该只是排序
(把主标题排在最前),不该是准入。不通过就退回原 names:
const eligible = (f?: string) => !!f && names.includes(f);
const lead = eligible(displayField) ? displayField
: eligible('name') ? 'name'
: eligible('title') ? 'title'
: undefined;这样 lead 的排序意图完整保留,只是不再兼作后门。
影响面判断
不是数据泄露,也不属于 #4419 那类「谓词被丢弃」——它产生的是一个更窄且语义错误的谓词
(搜到的是 id 里恰好含该子串的行)。真正的代价是:宣称的不变量没有被执行,
而 #4254 那道闸门在此之上给出的是一个它自己也不该给的通过。
Activity
os-zhuang commentedon Aug 1, 2026
交接说明(进行中)
分支:
claude/v17-verification-defects-gnf9e6-data-envelope提交:
f566a1d fix(spec): the $search auto field set's lead ORDERS the set, it must not admit one (#4483)已完成
packages/spec/src/data/search-fields.ts的autoDefaultFields:lead现在从已过滤的names里挑,而不是只做存在性检查。三条旁路(displayField/fields.name/fields.title)统一过同一条谓词,排序意图完整保留,不再兼作准入后门。packages/spec/src/data/search-fields.test.ts(10 个用例)。已实测修复前 6 个失败、修复后全绿,覆盖:{ id: text, amount: number }+displayField: 'id'→allowed: []);SEARCH_AUTO_EXCLUDED_FIELDS每一个成员当displayField都不得进入;hidden的 / 类型不可搜的 display 字段不得进入;name/title两条旁路同样受谓词约束;searchableFields走declared分支不受影响;$searchFields=id在 REST 读路径:searchFields/groupBy/aggregations指向不存在的字段时被静默降级(#4226 收口后剩下的三条轴) #4254 那道闸门上重新被拒。pnpm --filter @objectstack/spec test全绿(286 files / 7232 tests)。当前卡在哪
算力额度可能耗尽,先把改动推到远端。这一条本身的代码与测试已经完成,尚未做的收尾:
下一步具体该做什么
packages/spec,按 AGENTS.md 跑:pnpm --filter @objectstack/spec build→check:generated→ 只对证明过期的产物--fix。(预期无产物变化:没有改.describe(),也没有新增/删除导出。)pnpm typecheck。pnpm dev -- --fresh -p 38103,对一个「唯一文本列是id」的对象发$search,确认下发给 driver 的 AST 里不再出现{ id: { $contains: … } };并确认$searchFields=id返回 400 而不是被接受。用完kill $(lsof -ti tcp:38103)。Generated by Claude Code
chore: add changeset for the v17 REST envelope defects (#4431, #4435, #…
fix(data,runtime,drivers): REST 信封与 $search 字段集 (objectstack-ai#4431, o…
test(spec): the last data/ file group's test titles state each cited …