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:491 用 JSON.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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions