Follow-up to #41 (PR #48). Discovery was narrowed to tracked paths; the bytes executed at those paths were not.
Problem
scripts/gates/structure.sh Stages C and D select paths with git ls-files --cached, then hand each path to python3 -m unittest or to bash. Both open the working-tree file. Index membership of a path says nothing about the content executed.
Exploitation: overwrite the body of the tracked scriptorium/skills/docs/scripts/test_check_markdown.py with arbitrary Python and do not stage it. Stage C selects the path from the index and executes the modified content. Identically for Stage D: overwrite scripts/gates/test_structure.sh in place without staging and it runs. A tracked symlink whose target is untracked working-tree content has the same shape.
Why this is worth fixing
Same threat model as #41: the gate is the test binding in .daedalus/config.json, so it runs inside the Daedalus pipeline where an implementer subagent writes files into a worktree before any human reads the diff. In that model the writer has arbitrary worktree write, so #41's control reduces "introduce any new executed path" to "overwrite one of the existing tracked files", which is not a meaningful barrier for an automated writer.
Raised as a HIGH finding by the security review on PR #48 and dispositioned out of scope there, correctly: the fix is a different change from the one #41 specified. The merged Discovery rule: header paragraph in structure.sh already states the limitation honestly ("What this does NOT buy: pinning the CONTENT of a path that is already tracked"), so the gap is documented in the script but was not tracked in an issue until now.
Relationship to #49
#49 covers a different vector: untracked sibling modules imported via Python's sys.path[0]. Its acceptance criteria are satisfiable with python3 -P or -I alone, which does not close this issue, and closing this issue does not by itself close #49.
One fix closes both: execute from a clean checkout of the index (git archive or a throwaway git worktree) rather than from the working tree. That candidate is already listed in #49's Notes. Whichever lands first should state in its PR which of the two vectors it closes, so the other is not assumed covered.
Acceptance Criteria
Follow-up to #41 (PR #48). Discovery was narrowed to tracked paths; the bytes executed at those paths were not.
Problem
scripts/gates/structure.shStages C and D select paths withgit ls-files --cached, then hand each path topython3 -m unittestor tobash. Both open the working-tree file. Index membership of a path says nothing about the content executed.Exploitation: overwrite the body of the tracked
scriptorium/skills/docs/scripts/test_check_markdown.pywith arbitrary Python and do not stage it. Stage C selects the path from the index and executes the modified content. Identically for Stage D: overwritescripts/gates/test_structure.shin place without staging and it runs. A tracked symlink whose target is untracked working-tree content has the same shape.Why this is worth fixing
Same threat model as #41: the gate is the
testbinding in.daedalus/config.json, so it runs inside the Daedalus pipeline where an implementer subagent writes files into a worktree before any human reads the diff. In that model the writer has arbitrary worktree write, so #41's control reduces "introduce any new executed path" to "overwrite one of the existing tracked files", which is not a meaningful barrier for an automated writer.Raised as a HIGH finding by the security review on PR #48 and dispositioned out of scope there, correctly: the fix is a different change from the one #41 specified. The merged
Discovery rule:header paragraph instructure.shalready states the limitation honestly ("What this does NOT buy: pinning the CONTENT of a path that is already tracked"), so the gap is documented in the script but was not tracked in an issue until now.Relationship to #49
#49 covers a different vector: untracked sibling modules imported via Python's
sys.path[0]. Its acceptance criteria are satisfiable withpython3 -Por-Ialone, which does not close this issue, and closing this issue does not by itself close #49.One fix closes both: execute from a clean checkout of the index (
git archiveor a throwawaygit worktree) rather than from the working tree. That candidate is already listed in #49's Notes. Whichever lands first should state in its PR which of the two vectors it closes, so the other is not assumed covered.Acceptance Criteria
scripts/gates/test_*.sh.case_untracked_python_refusedpattern established in Stop the structure gate from executing untracked test files #41.Discovery rule:header paragraph instructure.shis updated to describe the guarantee that now holds, replacing its current "What this does NOT buy" clause about unstaged content.