Skip to content

authorable-surface.json 在 main 上不是 gen:schema 的输出 —— 基线手编的字节级实锤(#4650 加固建议) #4663

Description

@os-zhuang

#4653(双源 C5)落地时掉出来的范围外发现,不在 #4653 的 PR(#4662)里修,单独立案。归 #4650「基线手编漏洞」的加固范围。

事实:authorable-surface.json 在 main 上不是生成器的输出

scripts/build-schemas.ts:491JSON.stringify(updated, null, 2) 写这个文件。JSON.stringify 不转义非 ASCII,所以生成器写出的 description 行里是字面 (U+2014)。

同一个脚本、同一个写法写出的姊妹文件 json-schema.manifest.json,在 main 上确实是字面 。但 authorable-surface.json 在 main 上是转义形式 —— 也就是说,它在某一刻被生成器以外的东西写过。

#4662 里我只跑了 pnpm gen:schema,没碰这个文件,而它的 description 行就自动变回了字面形式:

-  "description": "Ratchet of every AUTHORABLE key in the spec — what a metadata author may write, ...
+  "description": "Ratchet of every AUTHORABLE key in the spec — what a metadata author may write, ...

转义是什么时候进来的

逐 commit 检查该文件首行的转义状态:

commit 状态
355e951e1 字面(生成器输出)
a2cd18af1#4587 → PR #4603(双源 C2) 字面(生成器输出)
e533b0b1b#4583 → PR #4601(retire datasource.capabilities) 转义(非生成器输出)
0c0fbd952 / c13350b47 / 0a936ea62 / 21676eb5d 转义,一路带到 main

e533b0b1b 同时把 key 数从 8304 → 8291(−13)。那正是一个 retire 单,减 key 本身合理;但减 key 的同时该文件不再是生成器输出,说明这一版是手工写/手工改出来的,而不是 gen:schema 重写出来的。

为什么这值得单独记一笔

#4535 §2 与 #4650 已经指出「删基线行就是删证据,门禁不会拦」,但那是推断;这里是实锤:文件在 main 上的字节形态自证它被生成器以外的东西写过,而且是在一个减 key 的 commit 里。

顺带说明这个洞的检测成本几乎为零 —— 只要要求该文件必须与 JSON.stringify(gen(), null, 2) 逐字节相等,手编就无所遁形,不需要去判断被删的行是不是 aged-out tombstone。建议 #4650 的加固至少包含这条字节级 round-trip 检查,它比语义检查更难绕过。

(#4662 已顺手把该行带回生成器形态 —— 那是 gen:schema 自己写的,不是我改的。)

关联:#4650(门禁加固本体)、#4535 §2、#4601 / #4583#4653 / #4662、ADR-0104

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions