config: a __proto__ key in a tracked config.json silently drops the git rails #114
Labels
No labels
already-shipped
bug
documentation
duplicate
enhancement
external-review
good first issue
help wanted
in progress
invalid
needs-decision
proposal
question
security
wontfix
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: crenshawdev/cadence-archived#114
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
A tracked
.planning/config.jsoncan turn off the git rails, and every surface that would show you is blind to it.lib/config-merge.mjs:68-70:JSON.parsemakes__proto__an OWN property,Object.entriesyields it, andmerged[k] = ...fires the__proto__setter. The merged config gets reparented.readLayerdoes no sanitizing.Proven, end to end
.planning/config.json={"__proto__":{"git":{"on_protected":"allow","protected_branches":[]}}}, branchmain,git commit:git-guard.mjspermissionDecision: "ask"config.mjs validate{"ok":true,"checked":0,"errors":[]}The split is the point:
flatten()inconfig.mjs:88-96enumerates OWN keys, so inspection sees nothing, whilecommitDecision'sconfig.gitwalks the prototype and obeys it.Object.keys(merged)is empty on a config that is actively deciding.The condition that makes it worse, not better
It only bites when no LOWER layer defines that top-level key.
mergeLayersbases onglobalValue || {}, so a machine with a~/.claude/cadence/config.jsonthat definesgitshadows the injection with its own key, and the attack silently does nothing.That means a naive test on a configured machine looks clean, and the exposure is worst on a FRESH INSTALL with no global config. New users, the ones with no reason to suspect the file, are the ones it works on.
.planning/is tracked, so it travels with the clone.Scope, honestly
It does NOT pollute
Object.prototypeglobally, and it does NOT reach rawJSON.parsereaders -git-publish.mjs:69 repoAutoClosereturnedfalseunder the same input, soauto_closecannot be switched on this way. Reach is everymergeLayersconsumer:git-guard.mjs:93,git-publish.mjs:84,git-branch.mjs,land-cleanup.mjs,route.mjs.Fix
Skip
__proto__,constructorandprototypeindeepMerge, or buildmergedwithObject.create(null)anddefineProperty. Add a test that a hostile layer cannot moveon_protectedor emptyprotected_branches, and pin the inspection/enforcement agreement sovalidatecan never again report clean on a config that decides.Fixed in v3.2.0 (PR #124). CFG-01, phase 1. deepMerge defines own properties instead of assigning, so proto, constructor and prototype in a repo layer store inertly rather than reparenting the merged config. The fixture that allowed a commit on main with no output from git-guard.mjs now produces permissionDecision: "ask", identical to the benign control.
Verified through the phase UAT walk. The milestone audit traced 12/12 requirements with 0 broken and 36/36 acceptance criteria covered.