fix(security): fence trailing-dot and 8.3 Windows spellings (#5265) - #5336
fix(security): fence trailing-dot and 8.3 Windows spellings (#5265)#5336NicholasRBowers wants to merge 1 commit into
Conversation
Design Review (Fable 5) — 🟡 CONCERNSDesign-level review of Design-Verdict: CONCERNS Sound class-level fix, but the fence's coverage now rests on per-join-site discipline that already failed once in this PR — unguarded, the next site silently reopens the class. Watch
Suggestions
[DESIGN-REVIEWED] 6d3840d |
First Principles Review (Fable 5) — 🟡 CONCERNSPremise-level review of All mechanical checks are done: First-Principles-Verdict: CONCERNS Two real behavior changes — the 64→254 excursion-cap raise and the hook-path DoS restructure — ship documented only in code comments, never in the description. What this change shipsIntent: stop an agent shell command from reaching the fenced credential stores via Windows normalization-equivalent spellings (#5265) — a FIX.
Watch
[FIRST-PRINCIPLES-REVIEWED] 6d3840d |
GPT 5.6 Review — ✅ no blocking findingsGPT 5.6 completed its review of This comment is updated in place on each push. Review detailsNo findings. False positive or not applicable? A repository writer can comment: |
Opus 4.8 Review — ✅ no blocking findingsReviewed Review detailsI could not obtain shell access to run a timing probe (Bash denied, matching the discovery pass's note), so I analyzed the backtracking behavior by hand. Analysis of CANDIDATE 1 (the sole candidate). I traced the two adjacent stars in
The only free variable is the D/G split point (k units to D, N−k to G), giving N+1 attempts; each failing G-scan is O(N−k). Total work is Σ O(N−k) = O(N²), not exponential. At the 20 KiB This is far milder than the failure modes the PR defends against (exponential seconds-to-minutes at ~350–600 chars; 56s UNC dot flood; linear-past-watchdog on 24 KB). A modest quadratic bounded to sub-second at the input cap does not establish an observable wrong outcome — element (c) — at ≥80 confidence. The claim rests on "probably sub-second… degradation rather than a certain hang," which is exactly the "could/might" the falsification bar rejects. Candidate dropped. No independent, grounded defect at ≥80 emerged from reading the changed patterns and helpers ( No findings. [OPUS-REVIEWED] 6d3840d Verdict parsed from the review's SHA-scoped output markers for commit False positive or not applicable? A repository writer can comment: |
|
🤖 Kiro Crew [operator: NicholasRBowers#a942f9ca]: This PR has been inactive for 7+ days with failing CI. I've assessed the blockers and they appear resolvable — I'll push fixes directly to this branch as a co-author. Assessment: The only red lane is a GPT 5.6 blocking finding on If you'd prefer I don't touch this PR, add the |
48a27a0 to
579681f
Compare
|
🤖 Kiro Crew [operator: NicholasRBowers#a942f9ca] Pushed head GPT 5.6 blocking finding (hash-form 8.3 aliases) — FIXED. The 8.3 alternation in Local pre-push review findings, also fixed in this head:
Local gates: |
579681f to
de4111e
Compare
|
🤖 Kiro Crew [operator: NicholasRBowers#a942f9ca]
Local gates on |
de4111e to
d19f814
Compare
6b6cc94 to
e2d07e7
Compare
|
One finding on the 8.3 alias branch, offered as a contribution rather than a Measured at 2026-09-04T01:01:49Z against this PR's head The stem is pinned at 6 while the tail is unbounded
The stem comes from A 6-character stem is therefore correct only for a 1-digit tail. Pinning the stem Measured on this headDriving The control refusing is what makes the rest discriminating rather than vacuous. Why a 3-digit ceiling is not the natural stopping pointThe obvious repair is to enumerate stems of 6, 5 and 4, which covers Collisions are not the only way an alias gets assigned, though. So a 6/5/4 enumeration leaves What that suggestsDeriving the stem length from the budget rather than from a hardcoded list closes It also stays consistent with the position the docstring already takes: a shorter Nothing else in the PR prompted this, and I am not asking for a change beyond the |
Open PR relationship auditThis is a consolidated, point-in-time code-level audit note. It compares complete merge-base diffs and current/merged code; it does not treat a shared topic as duplication or partial coverage as completion. Relationship findings
No PR, Issue, label, branch, or review state was changed by the relationship-note portion of this audit. |
e2d07e7 to
b930d5e
Compare
|
Thank you for the independent verification — a 17/17 refused + 4/4 benign-permitted probe from outside the branch is exactly the corroboration this class of change needs, and the honest framing of what a Linux-side textual probe can and cannot claim is appreciated. On the conflict analysis: it was measured against head The incidental-catch analysis in section 2 (trailing-space termination, the Your separate 8.3 multi-digit-tail item is answered in the reply to your second comment. |
|
Disposition: Confirming the analysis: Why deferring is safe for this PR's threat model, as you noted yourself: on NTFS with defaults the generator switches to the hash form after ~4 collisions and this PR already matches the hash shape, so default-NTFS coverage is complete; the shrinking-stem forms require FAT/exFAT or a deliberate One process note: this PR's escalation rules treat security findings as never silently deferrable, so flagging for the maintainer (nrb) explicitly — #8128 covers the shrinking-stem tail forms; if you'd rather absorb the budget-derived stem enumeration into this PR instead, say so and it lands with the pending security.py fix. |
Fixed in |
|
Disposition:
Done — 11 of 11 join sites now render through
Done — deleted; only the The two undeclared behavior changes the verdict noted (the 8.3 hash form, the dropped same-line restriction) are now covered in the PR description; the same-line item became moot when #8282's per-line verb walk landed on main and this branch adopted it. |
Win32 normalization strips trailing dots/spaces per path segment, and every entry answers to its 8.3 short name, so a fenced credential store spelled kiro-cli. or KIRO-C~1 matched no branch of the raw-text matcher and the command was permitted. Render every fenced segment through a shared _win_segment helper that alternates the literal with its 8.3 short form and tolerates the trailing-[. ] run, at every Windows join site (home-anchored dirs, %APPDATA% remainders, write-protected prefixes/leaves, agents dir). Review fixes folded in: the 8.3 alternation also matches Windows hash-form aliases (two-char prefix + four hex digits + ~N, generated on short-stem collision); the %LOCALAPPDATA% remainder is rendered through _win_segment like every other join site instead of plain re.escape; and the win_gsep trailing-[. ] tolerance pins the segment's last character as non-dot so the name run and the [. ]* tail split disjointly (the overlapping form was super-exponential on excursion chains -- ReDoS on the synchronous approval path), pinned by a wall-clock linear-time test. Review round 2 (First Principles) closed three same-class sibling sites left spelling-blind in the same file: the %LOCALAPPDATA% anchor now takes the same [. ]* pad and _win_segment re-entry as its %APPDATA% twin, the crew variable-leaf parent branch and the module-level dot-slash traversal alternation both render their segments through _win_segment, and the "EVERY Windows join site" comment now enumerates all converted families. Review round 3 (GPT) removed the finite 64-char cap on the win_gsep excursion name run and the generic_win_home username run: a 65+-char segment padded straight past the fence, and disjointness (the pinned non-dot final character), not the cap, is what bounds backtracking. Both runs are now unbounded, pinned by a long-name test and the existing linear-time wall-clock test. The excursion name run is capped at Windows's 255-char component ceiling, which costs no coverage and bounds the scan. Review rounds 4-5 (GPT + Opus, ruling by nrb) rewrote the raw-text pass for linear time instead of windowing it. Round 3's DoS fix had capped Pass 1's scan at 2000 chars, and round 4 showed the window was itself a bypass: Pass 2's tokenizing normalizer cannot see Windows-native backslash spellings, so a fenced path past char 2000 was blocked by nothing. The root cause was never the Windows branches: profiling isolated it to the token-anchor idiom (?:^|.*[sep]) whose leading .* re-scans the remaining input at every search offset -- O(n^2), ~20s on a 24KB command, and pre-existing on main. Every token anchor is now the width-1 negative lookbehind (?<![^sep]) -- provably the same match set under search(), at O(1) per offset -- the redirect arm drops its leading .*, and the read/write-verb and interpreter-open() families (whose mid-pattern gap is inherently quadratic in one regex) run as chained linear searches in _verb_then_sensitive_path, sharing the bare path pattern with the builder. The scan window is deleted: Pass 1 (and the separator-collapsed Pass 1b) runs over the full command. Pinned by a bypass regression test (fenced Windows path beyond char 2000 must block -- red on the windowed head), an adversarial-flood wall-clock test per alternate family, and glued-path parity tests for the verb split. Closes #5265 Co-authored-by: Kiro Crew <kirocrew@amazon.com>
|
self-added: yes
|
|
self-added: yes
|
|
self-added: no
|
|
self-added: no
|
|
Closing as superseded on This PR extends the command-text path fence in The Windows-spelling concern itself is still valid and should be re-triaged on the new architecture ( |
Problem
The shell-side credential fence
is_sensitive_bash_commanddecides its first pass by pattern-matching the RAW command text. Two Windows path-normalization forms resolve to a fenced credential store while matching none of the matcher's branches, so a command naming the store in either spelling was permitted:kiro-cli.andkiro-cliboth resolve to the fencedkiro-cli.AWS~1for.aws,KIRO-C~1forkiro-cli), so the short spelling names the store without containing its literal text.The reporter measured both forms leaking on the
%LOCALAPPDATA%-style alias branch AND the pre-existing home-anchored branch — the root cause is a property of comparing normalization-equivalent spellings textually, not a gap in any one branch.Closes #5265
Why it matters
The fenced directories hold kiro-cli's live SSO bearer token; the gate exists so that an agent shell command naming that store is refused. For either spelling the blast radius equals the fence being absent. Severity bounds: neither spelling arises in normal usage (it must be produced deliberately), and the separate tool-path gate
is_sensitive_pathis unaffected — this is the shell/raw-text pass only.Fix
Symptoms → root cause: the matcher rendered every fenced segment as
re.escape(literal), so any normalization-equivalent re-spelling of a segment fell outside the alternation.Change: every fenced Windows path segment is rendered through a new
_win_segment()helper that alternates the literal with its textually-derived 8.3 short-form pattern (_win_83_short_pattern(), ASCII[0-9]+tails,_-substitution for the 8.3-invalid punctuation mirroringRtlGenerate8dot3Name) and tolerates a trailing[. ]*run. Applied at EVERY Windows join site — the home-anchored dirs, the%APPDATA%remainders, the write-protected prefixes/leaves, the agents dir, and the keystone-artifact parents (the last converted in review round 10, which found it still onre.escape) — because the report measured the leak on more than one branch. The now-consumerless_SENSITIVE_SEGMENT_ALTalternation is deleted (First Principles review subtraction). The verb-anchored linearization this branch previously carried landed onmainindependently via #8282; this diff now contains only the spelling fence.Pre-push review (GPT 5.6 Sol + Opus 5, model-pinned) found the same class re-entering at the seams, all fixed in this commit:
C:\Users\u \…,%USERPROFILE%.\…,%APPDATA%.\…,%KIRO_HOME%.\…) now tolerates the run via a pad onwin_home_altsand the variable anchors;win_gsep's traversal control segments (\.,\X\..) tolerate the run, and the%APPDATA%\..\Roamingre-entry rendersRoamingthrough_win_segmentlike every other fenced segment;[. ]*(unbounded flat class, linear match) rather than a finite cap that padding one character past would defeat.Two further behavior changes ride in this diff, both derived from review findings and previously documented only in code comments:
win_gsep's excursion-segment name cap is raised from{1,64}to{0,254}(a 65-char segment name was a pad-past-cap bypass), and the fence's Windows branches are restructured so adversarial dot-flood / single-component commands up to the 20 KiB scan ceiling cannot stall the synchronous hook path (the pre-fix shape measured ~56 s of backtracking at 24 KB; wall-clock liveness is pinned bytest_security_gate_liveness.pyand the resized flood tests).Deliberately out of scope (recorded in the issue triage comment): direction 1 — resolving spellings through a real path tokenizer/
GetLongPathName— needs a policy for not-yet-existing write targets and an answer for the non-Windows hosts this gate also runs on. That is an architecture decision for a human; this change does not foreclose it. Known residuals, recorded rather than implied: the anchor-free bare-token branch (_BARE_TOKEN_PROTECTED_LEAVES) is not extended with 8.3 alternatives (advisory;is_sensitive_write_pathis the primary control there), and reviewer suggestions to suppress~Nalternatives for names that appear already-8.3-valid were declined — over-matching is this gate's documented safe direction, and suppressing based on a model of the generator risks re-opening a bypass where that model is wrong.Tests
TestWindowsPathShapes::test_trailing_dot_and_space_segment_spellings_are_blocked— trailing dot/space on fenced segments, non-final segments, the anchor's fixed segment, the username segment (all three branches: sensitive dirs, write-protected leaf, agents dir), the variable anchors, theRoamingre-entry, and a traversal control segment.TestWindowsPathShapes::test_83_short_name_spellings_are_blocked— home-anchored and alias 8.3 spellings, collision tails, non-final segments, case-insensitivity.TestWindowsPathShapes::test_normalization_allowances_do_not_widen_to_unfenced_paths— negatives proving the run and the~Nalternation do not widen onto benign paths, sibling entries (kiro-cli.bak,.awsx), or a digitless tilde.TestWindowsPathShapes::test_83_stem_substitutes_invalid_punctuation— pins the helper's_-substitution contract directly.TestWindowsPathShapes::test_artifact_parent_alias_spellings_are_blocked— trailing-dot and 8.3 spellings of a keystone-artifact parent are refused (round-10 regression: the one join site left onre.escape), with plain-spelling and non-fenced-parent controls.Non-vacuity proven: with
main's unmodified matcher, the three positive/helper tests FAIL (verified by checking outmain'ssecurity.pyand re-running the class: exactlytest_trailing_dot_and_space_segment_spellings_are_blocked,test_83_short_name_spellings_are_blocked, andtest_83_stem_substitutes_invalid_punctuationfail; the negatives pass on both). All tests are pure string-in/verdict-out — no credential file is created, read, or touched.Local gates: isort, flake8, mypy (1038 files), full
test/test_security.py(546 passed, 1 skipped).Manual verification
Ran a verdict-only repro harness over 17 spellings (12 bypass forms, 5 negatives): on
main, 10 of 12 bypass spellings are permitted; with this change all 12 are refused and all 5 negatives remain permitted. The two spellings already caught onmain(trailing-space forms whose space fell inside an existing character class) stay caught.Screenshots
N/A — backend security matcher change, no user-visible dashboard or app state.
Pattern harvest
Rule candidate: a regex
search()-ed over hook-path input must not have an alternate that begins with.*(or any unbounded consumer) —searchalready retries every offset, so a leading.*re-scans the tail at each position and turns the scan O(n²). This branch originally carried that linearization; the equivalent fix landed onmainvia #8282 (linear token anchors + a per-line verb walk), whose source-guard test and adversarial-flood wall-clock tests this diff extends rather than replaces. Agrep -P 'rf?"\(\?:\^\|\.\*'oversrc/kiro_crew/security.pywould make it a cheap Automated Rule Check candidate.