Repository navigation
[finding] the third __proto__ site: AssignmentConfigSchema's own .catchall() silently drops a TOP-LEVEL flow variable named __proto__ #19151
Description
Path: none | instrument-adjacent contract (spec parse surface) | 北极星「优先级」4
Filed by the domain:spec execution seat (seat 3, session_019srGWGCBBCBHqcDoRZpQRh) at the shift boundary. Reported in PR #19147's body by its implementing dev, which then DIED before filing it (container restarted about 23:20Z); confirmed independently by the PR's at-tier contract reviewer (record 5737516015). ⛔ Unassigned and ungraded — grading is triage's.
The third site of the #17852 family, and the one the ruling did not reach. AssignmentConfigSchema's own OBJECT carries .catchall(z.unknown()) and its top-level keys may be author-named flow variables — and zod's catchall branch skips a __proto__ own key exactly the way its record branch does. ⇒ a variable named __proto__ declared at the TOP level of an assignment node config is silently dropped from the parsed config while the parse reports success.
The two halves, and whose reading each is
In-tree half — this seat's own reading, packages/spec/src/automation/builtin-node-config.zod.ts (read on origin/main, 2026-09-18 22:01 UTC, and unchanged by PR #19147 apart from the assignments slot):
export const AssignmentConfigSchema = lazySchema(() => z.object({
assignments: …,
})
.catchall(z.unknown()));
and its docblock, verbatim: 「Not strict: an assignment node is exempt from registerFlow()'s undeclared-key walk by design (its top-level keys may be variables), and a closed shape here would contradict the descriptor's additionalProperties: true the form↔Zod ledger pins.」 ⇒ the top level of this config is an authoring surface for variable names, by design and on purpose.
Vendor half — NOT this seat's reading, attributed: the PR's at-tier reviewer reports handleCatchall skipping __proto__ at zod core/schemas.js:767-769 (it pulled the 4.4.3 tarball; this container has no node_modules). The same mechanism one function over from $ZodRecord's if (key === "__proto__") continue;, which #17852 measured and fixed.
Why it is a separate card rather than part of #17852
Maintainer ruling A-narrow (comment 5725370319 on #17852) named exactly two slots — ObjectSchema.fields and AssignmentConfigSchema.assignments — and said in terms: 「⛔ No sweep over the 398 records; a further record keyed by author-named identifiers is its own card when measured」. PR #19147 refuses __proto__ at the assignments RECORD; this is the enclosing OBJECT's catchall, a different mechanism at a different depth, and it was named as a measurement lead in that round's dispatch (5736756482) precisely so it would be filed rather than folded in.
What is NOT claimed
- ⛔ NOT measured: whether any authored flow anywhere declares a top-level key named
__proto__on an assignment node. The finding(spec): zod z.record() SILENTLY DROPS a__proto__key from its parse OUTPUT while reporting success — ObjectSchema accepts the document and hands back a different one #17852 census coveredfieldskeys andassignmentsvariable names, not this depth;cloudandhotcrmwere outside that container's reach entirely. - ⛔ NOT claimed that anything is broken for a user today. This is the same latent shape finding(spec): zod z.record() SILENTLY DROPS a
__proto__key from its parse OUTPUT while reporting success — ObjectSchema accepts the document and hands back a different one #17852 was first filed as — and that card's own history is the warning: it was filed 「LATENT today」 and a later round measured live consumers. - ⛔ NOT a prototype pollution: the drop is a read-side omission, never an assignment.
- ⛔ No mechanism proposed. The guard PR spec: pre-parse __proto__ guard on ObjectSchema.fields and AssignmentConfigSchema.assignments (#17852, #18847) #19147 lands (
refuseRecordProtoKey) is a record-level wrapper; whether it composes onto an object's catchall, or wants a sibling, is the card's first question, not its premise.
One more hazard the same reviewer raised, recorded here because it shares the subject
zod's treeifyError and error.format() throw a TypeError on any issue path containing __proto__ (reviewer's reading, record 5737516015). It named zero consumers in reach that call either on these schemas, and a one-line remedy (path: [] on the guard's issue).
Dedupe words
catchall __proto__ skip · AssignmentConfig top-level variable dropped · handleCatchall proto · assignment node catchall authoring surface · treeifyError proto TypeError
Related: #17852 (the record-level defect and its ruling) · #18847 (the assignments slot) · PR #19147 (implements both; carries this as an unfiled finding in its body) · #18670 (the JSON Schema projection gap).
Generated by Claude Code in session session_019srGWGCBBCBHqcDoRZpQRh; attribution is prose because a footer block is stripped on issue creation.
Generated by Claude Code
Activity
fix(pm): post-stamped tolerates the footer blank-line collapse, and i…
fix(spec): refuse a top-level `__proto__` on the `assignment` node co…
test(spec): automation test titles state each cited decision in words…