fix(security): make the push argument boundary shell-faithful - #8727
fix(security): make the push argument boundary shell-faithful#8727iamwhatever wants to merge 1 commit into
Conversation
Design Review (Fable 5) — 🟡 CONCERNSDesign-level review of Design-Verdict: CONCERNS Sound fix-forward for a real red-main security regression, but it grows the hand-rolled bash emulation into six must-agree quote-state automata inside one fail-closed gate. Watch
Suggestions
[DESIGN-REVIEWED] a478255 |
First Principles Review (Fable 5) — ✅ PASSPremise-level review of First-Principles-Verdict: PASS Nine red tests on main are the specification; every behavior change traces to one of them or to a review-found allow hole, and the deferral to #7832 is declared. What this change shipsIntent: turn main green by making the push gate's extractor and modeller derive the argument boundary once — a FIX.
Subtractions
[FIRST-PRINCIPLES-REVIEWED] a478255 |
GPT 5.6 Review — 🔴 changes requested (blocking)GPT 5.6 found at least one blocking issue that must be resolved before merging This comment is updated in place on each push. BLOCKING -- src/kiro_crew/security.py:6475 -- Case-pattern parentheses prematurely close process substitutions
Adjudication (Opus 4.8) — is blocking on each finding proportionate?F1 opens at Harm rung: UNBOUNDED — this is a bypass of the git-push protected-branch security control (governance/security ceiling), which the ladder answers without weighing. The remedy cost (distinguishing case-pattern terminators from construct-closing parens in the two process-substitution walks) is bounded, but for a security-class bypass I hold no authority to downgrade regardless. Conditions confirmed: splitter decrements on any False positive or not applicable? A repository writer can comment: |
Opus 4.8 Review — ✅ no blocking findingsReviewed Review detailsThe discovery pass produced no candidates, and my independent trace confirms the compensating controls:
I could ground no concrete false-negative, crash, or data-loss defect on the changed lines to the (a)/(b)/(c) bar at 80+. No findings. [OPUS-REVIEWED] a478255 Verdict parsed from the review's SHA-scoped output markers for commit False positive or not applicable? A repository writer can comment: |
569da37 to
b821e4d
Compare
|
|
Both deferral branches (the quoted-newline branch and the args-None branch) share
|
b821e4d to
be63963
Compare
The splitter's depth counter now increments on a plain unquoted
|
Character-blind blanking is gone. The new
|
Exactly the fix the review proposed:
|
be63963 to
023b7ff
Compare
Advisory, but a real allow-direction gap in the deferral seam, so fixed rather than deferred:
|
Nine tests failed on main in the git-publish floor, from opposite directions with one root cause: the positional argument list is derived from a command segment in one place (the extractor) and modelled in another (the walk), and the cross-merge of #8491 after #7808's dev base left the two disagreeing about where the argument list stops when the segment carries a shell metacharacter. Where the list stopped, and where it must stop: - The extractor returned operator-CUT words, so the walk -- written for raw spellings -- never saw the metacharacter it models: an extglob refspec arrived as the bare token before its paren and took the ambiguous-ref row instead of the wildcard row; a glued bare redirection lost the operator that consumes the next word; a lone background operator vanished into an empty token instead of poisoning the split. The extractor now returns RAW words and the walk dequotes and cuts them itself (one boundary, derived once). - The heredoc strip operator was matched as its two-character prefix, so its folded dash read as an attached target and the SEPARATED delimiter word survived as a phantom refspec, erasing the single-arg tag. The extractor's redirect grammar now folds the dash into the operator, mirroring the walk's. - Process substitution was ungated segment-wide, so a substitution that is a redirect TARGET -- consumed by the shell before git runs -- denied an ordinary feature push. It is position-dependent: a redirect target is stepped over; a WORD-slot substitution is kept as one argument and the walk lands it on the ungated branch, the same posture as before minus the false positive. - Segments were split on a quote-blind regex, so a quoted separator in a legal ref name cut the ref mid-word and the open-quote fragment escalated to the ungated deny. The split is now quote- and escape-aware; an unquoted newline still separates, so a newline REMAINING in a segment is bash splicing words across it -- ungated, unless a nested payload is itself a publish (a multi-line quoted script), in which case the payload walk judges it. - A git token that BEGINS inside a quoted span is a wrapper's payload text, so it no longer anchors the outer line as a push -- the nested-payload walk judges the quoted script, restoring the wrapper deferral for `bash -c '(cd /tmp && git push origin my-feature)'`. Every deferral is gated on the segment's TOP-LEVEL residue: an unquoted token carrying an expansion or a protected name means words in this shell would go unjudged, so the segment stays ungated. - A pure subshell-closer tail glued to a word (`my-feature)`) is punctuation after the word, not fusion into it: the word flows where a plain word would and a protected name under the glue is still read. A segment whose git ANCHOR itself carried operator glue escalates any finding to the ungated sentinel, so a paren-glued spelling of a protected push is never one per-rule opt-out away from an allow, while a clean feature push in a subshell stays allowed. - Process-substitution spans are measured with a quote-aware walk: a quoted paren in the body is data, and counting it stretched the construct over a trailing protected refspec (pre-push review). No protective reading was weakened: every change either widens what is denied (raw-word superset scans, glued-anchor escalation, deferral residue gate) or removes a reading the shell itself contradicts (redirect-target substitution, quoted separators, subshell closers). Before: test_push_branch_gate.py 6 failed / 97 passed; test_security.py -k TestGitPublishSubshellGluing 3 failed / 12 passed. After: 0 failed in both, full backend suite failure set identical to pristine main minus the nine. Closes #8695 Closes #8700
023b7ff to
a478255
Compare
|
|
🤖 Kiro Crew Auto-Pipeline [operator: iamwhatever] Closing this PR: superseded by #8712 (merged 2026-09-05T12:27Z, commit Verified on pristine
All 9 originally-failing tests are green on main, so this branch's reason to exist (unblocking the red main) is resolved. Rather than rebase a now-conflicting parallel rewrite of the same tokenizer onto the landed fix, standing down is the lower-risk path. One pointer for the record: GPT 5.6's final blocking finding here (span=3c5b15a2d41e, case-pattern |
Problem / Motivation
Main is red: nine tests fail on the git-publish push gate in
src/kiro_crew/security.py, from opposite directions with one root cause.test/test_push_branch_gate.py: 6 failed inTestUnrecognisedOptionsReadProtectively. These are fix(security): model publish option arity so the floor tag holds #7808's own assertions failing on arrival: shapes like an extglob refspec (@(main)), a heredoc strip operator (<<- EOF), a glued redirection (origin> log), a glued all-branches flag behind a redirect, and a background operator LOSE their protective floor tag — under-tagging, a security hole.test/test_security.py -k TestGitPublishSubshellGluing: 3 failed. These date from fix(security): close four enforcement bypasses in the bash command gates #7356 and were green before: a redirect-target process substitution (> >(tee log.txt)), path-qualified feature pushes, and quoted-operator ref names ('feature|x') are read as denials — over-blocking legitimate feature publishes.Why it matters
Every open PR inherits the failure through
refs/pull/N/merge(Backend Tests shards, Backend Lint & Type Check, Coverage Gate cascade), so no PR can reach a green rollup until this lands. The under-tagging half is additionally a per-rule-opt-out hazard on the push gate itself.What changed (motivation → approach → change)
Root cause: the positional argument list is derived in one place (
_git_push_args, reworked by #7356) and modelled in another (_push_segment_targets_protected, rewritten by #7808 on a pre-#7356 base). The cross-merge of 166ff5a (#8491) followed by eaa8a45 (#7808) left the two disagreeing about where the argument list stops when a segment carries a shell metacharacter: the extractor returned operator-CUT words and stripped constructs the walk was written to model on RAW spellings.The fix makes the boundary derived once, faithfully — the commit body carries the full per-metacharacter answer to "where does the list stop, and where must it stop". Summary:
_git_push_argsreturns RAW words; the walk dequotes/cuts them itself. The extractor's redirect grammar folds<<-'s dash into the operator, and keeps a process-substitution WORD (<(cmd)at word start) as one argument for the walk to land on the ungated branch, while a redirect-TARGET substitution is stepped over (the shell consumes it before git runs)._AMBIGUOUS_EXPANSION_RE(that segment-wide match is what over-blocked> >(tee log.txt)): it is position-dependent and now handled positionally._split_push_segments): a quoted;/|in a legal ref name is data. An unquoted newline still separates, so a newline remaining in a segment is a bash splice — ungated, unless a nested payload is itself a publish (a multi-line quoted script), which defers to the payload walk.gittoken that begins inside a quoted span is a wrapper's payload text and never anchors the outer line (_push_token_start_states); every deferral is gated on top-level residue (_push_top_level_residue_hazard), sobash -c '…feat…' & $x push origin mainstays ungated-denied whilebash -c '(cd /tmp && git push origin my-feature)'stays allowed.(git push origin main)) escalates any finding to the ungated sentinel via one shared anchor resolver (_resolve_push_anchor); a pure subshell-closer tail (my-feature)) is punctuation, not word fusion._paren_construct_end) — a quoted(in the body is data; counting it swallowed a trailing protected refspec.Items 4–6 and the quote-aware
$-residue reading (_push_token_expands) came out of two pre-push review rounds (gpt-5.6-sol + opus-5) plus a verifier round; both reviewer-found allow-direction holes are fixed and regression-tested. No protective reading was weakened: every change widens what is denied or removes a reading bash itself contradicts.The spec's stated ordering risk held: both failure directions were re-run after every change. FIX FORWARD as directed — #7808 is not reverted; its 749 test lines are the arity model this implements.
Tests
test_push_branch_gate.py115 passed,TestGitPublishSubshellGluing15 passed).TestArgumentBoundaryHelpers(12 tests): raw-word extractor contract,<<-delimiter consumption, process-substitution word retention, payload-quoted anchor gating, quote-aware segmentation, quoted-newline splice vs multi-line payload, glued-verb escalation vs clean subshell feature push, subshell-closer punctuation, quoted-paren process-substitution boundary (review round), wrapper-deferral residue gate (review round), ANSI-C$'…'span integrity.Manual verification
N/A — unit coverage sufficient: the gate is a pure function of the command string, and every reported shape (plus the review-round exploits) is pinned as a direct unit test.
Related Issues
Note: PR #8672 touches the same area of
src/kiro_crew/security.pyon a base predating #7808 — it does not carry the arity model and will inherit this red until it rebases onto this fix; a textual conflict there is expected and deliberately not absorbed here. Issue #7832 (bash-faithful word lexer) is the architectural successor and is intentionally out of scope.Closes #8695
Closes #8700
Pattern harvest
Rule candidate: review-prompt
Pattern: "two PRs each green on their own base, red together — a parser split across an extractor and a modeller must derive its boundary once; flag any PR that changes one of
_git_push_args/_push_segment_targets_protectedwithout running both directions' test files".Checklist
feat|fix|docs|refactor|perf|test|chore|ci|build|revert: ...)