Skip to content

fix(security): fence trailing-dot and 8.3 Windows spellings (#5265) - #5336

Closed
NicholasRBowers wants to merge 1 commit into
mainfrom
fix/win-path-normalization-fence-5265
Closed

fix(security): fence trailing-dot and 8.3 Windows spellings (#5265)#5336
NicholasRBowers wants to merge 1 commit into
mainfrom
fix/win-path-normalization-fence-5265

Conversation

@NicholasRBowers

@NicholasRBowers NicholasRBowers commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Problem

The shell-side credential fence is_sensitive_bash_command decides 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:

  1. Trailing dot or space on a path segment. Win32 strips trailing dots AND spaces from every segment, so kiro-cli. and kiro-cli both resolve to the fenced kiro-cli.
  2. 8.3 short name. Every entry answers to its DOS short form (AWS~1 for .aws, KIRO-C~1 for kiro-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_path is 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 mirroring RtlGenerate8dot3Name) 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 on re.escape) — because the report measured the leak on more than one branch. The now-consumerless _SENSITIVE_SEGMENT_ALT alternation is deleted (First Principles review subtraction). The verb-anchored linearization this branch previously carried landed on main independently 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:

  • the anchor's own final segment (C:\Users\u \…, %USERPROFILE%.\…, %APPDATA%.\…, %KIRO_HOME%.\…) now tolerates the run via a pad on win_home_alts and the variable anchors;
  • win_gsep's traversal control segments (\., \X\..) tolerate the run, and the %APPDATA%\..\Roaming re-entry renders Roaming through _win_segment like every other fenced segment;
  • the trailing run is [. ]* (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 by test_security_gate_liveness.py and 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_path is the primary control there), and reviewer suggestions to suppress ~N alternatives 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, the Roaming re-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 ~N alternation 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 on re.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 out main's security.py and re-running the class: exactly test_trailing_dot_and_space_segment_spellings_are_blocked, test_83_short_name_spellings_are_blocked, and test_83_stem_substitutes_invalid_punctuation fail; 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 on main (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) — search already 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 on main via #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. A grep -P 'rf?"\(\?:\^\|\.\*' over src/kiro_crew/security.py would make it a cheap Automated Rule Check candidate.

@github-actions

github-actions Bot commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Design Review (Fable 5) — 🟡 CONCERNS

Design-level review of 6d3840d1210244ba4f4a4b31e4ac922516325d3a — updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

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

  • The class is closed only if every current and future Windows join site renders through _win_segment; the PR's own round 10 found a site still on re.escape, and nothing structural (a test iterating join sites, or a lint rejecting re.escape(part) joins inside _build_sensitive_regex) turns the next omission red. The failure mode is a silent credential-fence gap — exactly the partial-fix pattern the description says it is avoiding.
  • The matcher's own review history (a re-entry gap or a measured ReDoS — ~56 s backtracking, a watchdog-terminating scan — per padding round) is evidence the textual approach is at its complexity ceiling; the deferred tokenizer/canonicalization decision the description records should get an owner and an issue, not just a triage comment, or the next normalization quirk replays this whole cycle.

Suggestions

  • Add a structural guard in this PR: assert no fenced-constant join in _build_sensitive_regex bypasses _win_segment (or assert a canary 8.3 spelling matches for every branch, driven by iterating the constants).
  • Extract the per-pad linearity invariant ("the token before [. ]* must not match .") into one documented helper or test rather than five prose comments, since it is the single rule every future pad must obey.

[DESIGN-REVIEWED] 6d3840d

@github-actions

github-actions Bot commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

First Principles Review (Fable 5) — 🟡 CONCERNS

Premise-level review of 6d3840d1210244ba4f4a4b31e4ac922516325d3a — why this exists and whether the shipped surface is the smallest honest version. Updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

All mechanical checks are done: _SENSITIVE_SEGMENT_ALT has 0 remaining consumers (only the _ANYSEP variant survives), every Windows join site now renders through _win_segment (grep win_gsep.join|_win_segment( — 12 sites, 0 left on re.escape except the declared bare-token residual at security.py:9758), and the one other re.escape(part) hit (line 15467) is the unrelated Slack-manifest matcher. Here is the review:

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 ships

Intent: stop an agent shell command from reaching the fenced credential stores via Windows normalization-equivalent spellings (#5265) — a FIX.

  1. Trailing-dot/space spellings of fenced paths now refused, all branches — justified
  2. 8.3 short-name spellings (incl. hash form) now refused — justified
  3. Every anchor (%APPDATA%, %LOCALAPPDATA%, %KIRO_HOME%, username, resolved home) tolerates the same respellings — justified
  4. Trailing-dot alias of a fenced leaf (computer_use.json.), previously a pinned deliberate gap, now refused — justified
  5. Excursion segments of 65–255 chars now fenced (old {1,64} cap was pad-defeatable) — undeclared
  6. ~24 KB dot-flood / single-component commands no longer stall the synchronous hook path — undeclared
  7. _SENSITIVE_SEGMENT_ALT deleted — justified; 0 consumers (grepped _SENSITIVE_SEGMENT_ALT, 2 hits, both _ANYSEP)
  8. Relative-traversal pass gains the same spellings — justified, same root cause
  9. Hash-form branch over-matches ??+4-hex+~N siblings of fenced stores — justified (documented safe direction)

Watch

  • The description's pre-push-review list ("found the same class re-entering at the seams") names three seams; the diff also raises win_gsep's name cap {1,64}{0,254} and restructures for the measured "~56 s at 24 KB" backtrack (security.py comments, "found in review"). Both are derived (pad-past-cap bypass; measured cost) and correctly shaped, but a human merging on the description alone never sees them.
  • The declared residual is real and counted: 1 unfixed sibling of the root cause, _BARE_TOKEN_PROTECTED_LEAVES at security.py:9758, still on re.escape — accepted-and-deferred with the primary control (is_sensitive_write_path) named.

[FIRST-PRINCIPLES-REVIEWED] 6d3840d

@github-actions

github-actions Bot commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

GPT 5.6 Review — ✅ no blocking findings

GPT 5.6 completed its review of 6d3840d1210244ba4f4a4b31e4ac922516325d3a and found no blocking issues.

This comment is updated in place on each push.

Review details

No findings.
[GPT-REVIEWED] 6d3840d

False positive or not applicable? A repository writer can comment:
/ai-review override gpt 6d3840d1210244ba4f4a4b31e4ac922516325d3a: <one-sentence reason>

@github-actions github-actions Bot added readiness: action required A blocking check or review needs attention and removed readiness: checking Automated validation is still running labels Aug 23, 2026
@github-actions

github-actions Bot commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Opus 4.8 Review — ✅ no blocking findings

Reviewed 6d3840d1210244ba4f4a4b31e4ac922516325d3a — this comment is updated in place on each push.

Review details

I 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 appdata_sensitive_path:

  • Dedicated star D: (?:{win_sep}\.\.[. ]*{win_sep}{_win_segment('Roaming')})* — each iteration consumes exactly one \..\Roaming unit; the [. ]* runs match zero on the crafted input, so D is deterministic (whole units only).
  • win_gsep G, over the same region (\..\Roaming)^m: its first iteration is forced to alt-a (\.[. ]* consuming \..), because alt-b (\<name>\..) cannot start at \.. — after \, the mandatory non-dot char [^\\/\s'\".] can never be satisfied before the next separator. Thereafter alt-b consumes each \Roaming\.. in exactly one way (name must reach the separator; trailing [. ]* is zero). So G has exactly ONE decomposition per region — no internal multiplicity.

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 MAX_SCANNABLE_COMMAND_CHARS ceiling the unit count N is bounded to ~1800 (each \..\Roaming ≈ 11 chars), so worst case is ≈ N²/2 ≈ 1.6M regex steps — sub-second. The %LOCALAPPDATA% twin is structurally identical (same bound). Dot-padding a unit only adds a constant factor (D or G owns a given dot-run, not both), not a higher polynomial degree.

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 (_win_83_short_pattern, _win_segment, the join-site conversions); the construction is a strict widening of a deny gate and is extensively tested.

No findings.

[OPUS-REVIEWED] 6d3840d

Verdict parsed from the review's SHA-scoped output markers for commit 6d3840d1210244ba4f4a4b31e4ac922516325d3a.

False positive or not applicable? A repository writer can comment:
/ai-review override fable 6d3840d1210244ba4f4a4b31e4ac922516325d3a: <one-sentence reason>

@bolichen97
bolichen97 enabled auto-merge (squash) August 24, 2026 06:56
@iamwhatever iamwhatever added the needs-pr-triage PR scanner: awaiting automated triage label Aug 28, 2026
@NicholasRBowers NicholasRBowers added drive-to-green PR claimed by drive-to-green pipeline and removed needs-pr-triage PR scanner: awaiting automated triage labels Aug 28, 2026
@NicholasRBowers

Copy link
Copy Markdown
Contributor Author

🤖 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 src/kiro_crew/security.py — the new 8.3 short-name fence pattern {stem}~[0-9]+ misses Windows' hash-form aliases (2-char + 4-hex stem generated on collision), a reachable fence bypass. Plan: extend the pattern to also match the hash-form stem before ~N, add a pinning test, rebase onto current main, and answer the First Principles advisory CONCERNS.

If you'd prefer I don't touch this PR, add the pr-no-autofix label.

@NicholasRBowers
NicholasRBowers force-pushed the fix/win-path-normalization-fence-5265 branch from 48a27a0 to 579681f Compare August 29, 2026 15:08
@github-actions github-actions Bot added readiness: checking Automated validation is still running and removed readiness: action required A blocking check or review needs attention labels Aug 29, 2026
@NicholasRBowers

Copy link
Copy Markdown
Contributor Author

🤖 Kiro Crew [operator: NicholasRBowers#a942f9ca]

Pushed head 579681ff6 — rebased onto current main, single commit, with the following fixes folded in:

GPT 5.6 blocking finding (hash-form 8.3 aliases) — FIXED. The 8.3 alternation in _win_segment now also matches Windows hash-form short names ({2-char prefix}[0-9A-Fa-f]{4}~N, generated when truncated stems collide, e.g. KI0F3D~1). Red-before-green pinned by test_83_hash_form_short_names_are_blocked (fails on the old head) plus a negative control proving the hash shape does not widen to unfenced names.

Local pre-push review findings, also fixed in this head:

  • %LOCALAPPDATA% remainder was still rendered with plain re.escape — blind to every spelling this PR fences. Now routed through _win_segment like every other join site (test_localappdata_normalization_spellings_are_blocked, red-before verified).
  • The trailing-[. ] tolerance inside win_gsep made the name run and the dot-tail overlap, which was super-exponential on excursion chains (empirically >120s at ~350 chars — ReDoS on the synchronous approval path). The segment's last character is now pinned non-dot so the two runs split disjointly; test_excursion_chains_match_in_linear_time wall-clock-pins it (times out at 120s without the fix, <1s with it).

Local gates: test_security.py 1003 passed / 1 skipped; isort, flake8, mypy, black all clean on changed files.

@github-actions github-actions Bot added readiness: passed Eligible automated validation passed for the current revision and removed readiness: checking Automated validation is still running labels Aug 29, 2026
@NicholasRBowers
NicholasRBowers force-pushed the fix/win-path-normalization-fence-5265 branch from 579681f to de4111e Compare August 29, 2026 15:45
@github-actions github-actions Bot added readiness: checking Automated validation is still running and removed readiness: passed Eligible automated validation passed for the current revision labels Aug 29, 2026
@NicholasRBowers

Copy link
Copy Markdown
Contributor Author

🤖 Kiro Crew [operator: NicholasRBowers#a942f9ca]

  • Three unfixed siblings of the declared root cause — disposition: fixed in de4111e3e.

All three verified real and same-class, so fixed in-PR rather than deferred:

  1. localappdata_sensitive_path — the anchor now takes the same [. ]* pad and _win_segment('Local') re-entry as its %APPDATA% twin (%LOCALAPPDATA%.\kiro-cli\…, \..\Local.\, \..\LOCAL~1\ all refused).
  2. win_crew_leaf_parents — segments render through _win_segment (crew.\%F%, .kiro.\crew\%F% refused).
  3. _SENSITIVE_SEGMENT_ALT_ANYSEP — segments render through _win_segment (..\..\AWS~1\credentials, ..\..\.aws.\credentials refused), with a negative control pinning that unfenced 8.3-shaped names stay unfenced.

Each fixed site has a red-before-green pinning test (all 3 new tests fail on the prior head, pass on de4111e3e); the linear-time wall-clock pin still passes with the new [. ]* runs (separator class excludes ., so the runs stay disjoint).

Subtraction also taken: the "EVERY Windows join site" comment now enumerates all converted families — with the three sites converted, the claim is true rather than shrunk.

Local gates on de4111e3e: test_security.py 1006 passed / 1 skipped; isort, flake8, mypy, black clean.

@github-actions github-actions Bot added readiness: action required A blocking check or review needs attention and removed readiness: checking Automated validation is still running labels Aug 29, 2026
@NicholasRBowers
NicholasRBowers force-pushed the fix/win-path-normalization-fence-5265 branch from de4111e to d19f814 Compare August 29, 2026 16:01
@github-actions github-actions Bot added readiness: checking Automated validation is still running and removed readiness: action required A blocking check or review needs attention labels Aug 29, 2026
@NicholasRBowers
NicholasRBowers force-pushed the fix/win-path-normalization-fence-5265 branch 2 times, most recently from 6b6cc94 to e2d07e7 Compare September 4, 2026 00:07
@github-actions github-actions Bot added readiness: checking Automated validation is still running readiness: action required A blocking check or review needs attention and removed readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running labels Sep 4, 2026
@chenmingwei23

Copy link
Copy Markdown
Contributor

One finding on the 8.3 alias branch, offered as a contribution rather than a
request for anything else in this PR. I read the branch closely because the
docstring's reasoning about the generator is careful, and the shape it settles on
looks off by the generator's own budget in one direction.

Measured at 2026-09-04T01:01:49Z against this PR's head
e2d07e7.

The stem is pinned at 6 while the tail is unbounded

_win_83_short_pattern builds, for kiro-cli:

(?:KIRO\-C~[0-9]+|KI[0-9A-Fa-f]{4}~[0-9]+)

The stem comes from _win_83_chars(base)[:6] and the tail is [0-9]+. But an
8.3 basis name has an 8-character budget SHARED between the stem and the ~N
tail, so the generator shrinks the stem as the tail grows:

len(stem) == 8 - 1 - len(digits)

A 6-character stem is therefore correct only for a 1-digit tail. Pinning the stem
at 6 and admitting any tail length matches a shape that cannot be generated
(KIRO-C~10 needs 9 characters) and misses every shape that can.

Measured on this head

Driving is_sensitive_bash_command with each spelling, Path.home() patched to
C:\Users\u (booleans only, no file contents read). Every alias below is a legal
8-character basis name:

REFUSED    kiro-cli  (8 chars)  long name, control
REFUSED    KIRO-C~1  (8 chars)  stem 6, 1-digit tail
PERMITTED  KIRO-~10  (8 chars)  stem 5, 2-digit tail
PERMITTED  KIRO~100  (8 chars)  stem 4, 3-digit tail
PERMITTED  KIR~1000  (8 chars)  stem 3, 4-digit tail
PERMITTED  KI~10000  (8 chars)  stem 2, 5-digit tail
PERMITTED  K~100000  (8 chars)  stem 1, 6-digit tail

The control refusing is what makes the rest discriminating rather than vacuous.

Why a 3-digit ceiling is not the natural stopping point

The obvious repair is to enumerate stems of 6, 5 and 4, which covers ~1 through
~999. That reads like a ceiling because reaching a 4-digit tail by collision
would need a thousand same-prefix entries in one directory, which does not happen
-- and on NTFS with default settings it happens even less, since the generator
switches to the HASH form after about 4 collisions and this branch already
matches that shape. Default NTFS coverage is complete either way.

Collisions are not the only way an alias gets assigned, though.
fsutil file setshortname writes an arbitrary valid 8.3 name onto an existing
entry, with no collision history required, so the digit count in the tail is
chosen rather than earned. KIR~1000 is a legal 8-character basis name with a
3-character stem, and the row above shows it is permitted today. The
shrinking-stem forms are also the FAT/exFAT continuation of the algorithm rather
than a purely hypothetical branch.

So a 6/5/4 enumeration leaves ~1000 and longer reachable by deliberate
assignment, and the ceiling is unstated rather than argued.

What that suggests

Deriving the stem length from the budget rather than from a hardcoded list closes
the whole range in one expression: for each tail length d in 1 through 6, admit
a stem of 8 - 1 - d filtered characters. That is the same alternation shape the
branch already builds, with the stem length varying instead of fixed, and it needs
no judgement about how many digits are plausible.

It also stays consistent with the position the docstring already takes: a shorter
stem can coincide with a sibling entry sharing that prefix, which is the same
over-matching the existing stem and hash branches accept as the safe direction for
a gate that blocks on naming alone.

Nothing else in the PR prompted this, and I am not asking for a change beyond the
branch above -- take it or leave it as you judge best for the PR's scope. I am
deliberately not leaving an approval or a change request; this is a finding, not a
verdict on the review.

@bolichen97

Copy link
Copy Markdown
Collaborator

Open PR relationship audit

This 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

  • This PR is OVERLAPPING with PR #4291. The goals differ or the implementations can complement each other; this is not a duplicate claim. Recommended action for PR #5336: MERGE_DISCUSSION. Different bypass class in the same anchor construction; complementary, conflicting only textually. Files: src/kiro_crew/security.py.
  • This PR is OVERLAPPING with PR #7913. The goals differ or the implementations can complement each other; this is not a duplicate claim. Recommended action for PR #5336: MERGE_DISCUSSION. Complementary changes to the same Pass 1b loop, conflicting only textually. Both should land; the second one merged must rebase. Files: src/kiro_crew/security.py.
  • This PR is OVERLAPPING with PR #8282. The goals differ or the implementations can complement each other; this is not a duplicate claim. Recommended action for PR #5336: MERGE_DISCUSSION. Both PRs independently rewrite the verb-anchored branch of _build_sensitive_regex into a chained-search helper and both drop the redirect arm's leading .*, and they conflict textually. Neither subsumes the other: 5336 owns the Issue #5265 spelling fence, 8282 owns the 20 KiB ceiling, the UNC anchor and the cron attribution. A maintainer should pick which PR owns the verb extraction and rebase the other onto it. Files: src/kiro_crew/security.py.

No PR, Issue, label, branch, or review state was changed by the relationship-note portion of this audit.

@github-actions github-actions Bot added readiness: checking Automated validation is still running readiness: action required A blocking check or review needs attention and removed readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running labels Sep 4, 2026
@NicholasRBowers
NicholasRBowers force-pushed the fix/win-path-normalization-fence-5265 branch from e2d07e7 to b930d5e Compare September 4, 2026 07:46
@github-actions github-actions Bot added readiness: checking Automated validation is still running and removed readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running labels Sep 4, 2026
@NicholasRBowers

Copy link
Copy Markdown
Contributor Author

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 e8340b037, and the branch has since been rebased five times (current head b930d5ed0, on current main). The single add/add hunk you identified — main's bounded symlink-resolution block vs. this branch's _get_sensitive_verb_path_re() helper — was indeed resolved keep-both during the second rebase, exactly as your scratch-worktree run predicted, and your section-1 probe results held on the resolved tree in our gates as well.

The incidental-catch analysis in section 2 (trailing-space termination, the ~-admitting drive-letter class, directory-first matching) is a useful map of why those three rows were already refused on main; noted for the PR record.

Your separate 8.3 multi-digit-tail item is answered in the reply to your second comment.

@NicholasRBowers

Copy link
Copy Markdown
Contributor Author

Disposition: accepted-and-deferred — the finding is correct and the fix is out of scope here by your own framing, tracked in #8128 (which you filed).

Confirming the analysis: _win_83_short_pattern derives the stem from _win_83_chars(base)[:6] and admits ~[0-9]+, but the 8.3 basis budget is len(stem) == 8 - 1 - len(digits), so the fixed-6 stem only matches 1-digit tails; KIRO-~10 and shorter-stem forms slip through, as your measured rows show. Your proposed repair — derive stem length from the budget per tail length d in 1..6 — is the right shape and consistent with the over-match-toward-deny direction this branch already takes.

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 fsutil file setshortname. That residual is real but distinct, and #8128 records it with the budget-derived fix sketch.

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.

@NicholasRBowers

Copy link
Copy Markdown
Contributor Author
  • fixed — span=3c5b15a2d41e — Keystone publish artifacts remain reachable through Windows aliases

win_dirs_pattern uses _win_segment(part), but win_artifact_parents_pattern still uses re.escape(part). Trailing-dot/8.3 parent -> HookManager.on_tool_call -> matcher returns no denial -> keystone temporary payload is accessible.

Fixed in afe0c4441: win_artifact_parents_pattern now renders every part through _win_segment(part), identical to its 10 sibling join sites — all 11 join sites are now converted, making the description's "applied at EVERY Windows join site" claim true. Regression pinned by TestWindowsPathShapes::test_artifact_parent_alias_spellings_are_blocked (trailing-dot and 8.3 parent spellings refused; plain spelling and a non-fenced parent as controls). The same commit also rebased onto current main (absorbing #8282's independent linearization, which this branch previously carried its own version of) and deleted the consumerless _SENSITIVE_SEGMENT_ALT.

@NicholasRBowers

Copy link
Copy Markdown
Contributor Author

Disposition: fixed — both subtractions from the First Principles CONCERNS verdict on b930d5ed0 are applied in afe0c4441:

Replace re.escape(part) at the win_artifact_parents_pattern join site with the existing _win_segment(part) — identical to its 10 converted siblings, and it makes the "EVERY join site" claim true.

Done — 11 of 11 join sites now render through _win_segment, with a regression test (test_artifact_parent_alias_spellings_are_blocked).

Delete _SENSITIVE_SEGMENT_ALT — zero consumers repo-wide.

Done — deleted; only the _ANYSEP twin remains, which the traversal matcher consumes.

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>
@NicholasRBowers

Copy link
Copy Markdown
Contributor Author

self-added: yes
mechanism: none added this round; one removed (the unc_prefix alternate inside win_home_alts)

  • fixed — span=3c5b15a2d41e — UNC alternate restores watchdog-crossing regex backtracking

Fixed in 6d3840d by REMOVAL, not repair: |{unc_prefix} is deleted from win_home_alts, exactly as the finding prescribes. The alternate was reintroduced by this PR's own round-10 conflict resolution (rebase over #8282), not by design — the dedicated linear {unc_prefix}{win_sep} arm in win_anchor already matches every string the removed alternate matched, so no coverage is lost.
The agents-dir branch now consumes win_anchor (which carries the linear UNC arm) instead of win_home_alts{win_gsep}, so UNC spellings of the agents dir stay fenced through the linear pairing.
Verified: main's own test_security_gate_liveness.py::test_each_construct_is_linear_in_the_command[unc-chain] — the test that exceeded its 120s deadline on this finding's construct — passes on 6d3840d, plus the full security suites (2717 tests) locally. This ruling covers the class: any future unc_prefix alternative placed adjacent to win_gsep re-creates the measured quadratic and is a defect, per the pairing invariant comment at win_anchor.

@NicholasRBowers

Copy link
Copy Markdown
Contributor Author

self-added: yes

  • fixed — UNC rides into win_home_alts undeclared, duplicating win_anchor's existing UNC arm

Same defect as the GPT lane's span=3c5b15a2d41e, same fix in 6d3840d: the undeclared |{unc_prefix} alternate inside win_home_alts is removed rather than repaired — it was an artifact of this PR's round-10 conflict resolution over #8282, and the dedicated linear {unc_prefix}{win_sep} arm in win_anchor already covers every UNC spelling it matched. The agents-dir branch consumes win_anchor again, so no branch loses UNC coverage.
The verdict's core observation — the alternate landed in the quadratic spelling the adjacent comment itself documents as measured-quadratic — is accepted as written; the removal makes the documented pairing invariant hold everywhere again.

@NicholasRBowers

Copy link
Copy Markdown
Contributor Author

self-added: no

  • accepted-and-deferred — Watch: per-join-site discipline unguarded; Suggestions: structural guard + pad-invariant extraction; tokenizer decision needs an owner

Both Suggestions are accepted and tracked in #9170 (deferred-finding, assigned, Due 2026-09-21): a structural guard so an unconverted join site turns red (canary 8.3/trailing-dot spelling per branch, or a source guard rejecting re.escape(part) joins in _build_sensitive_regex), and the pad-linearity invariant extracted from five prose comments into one enforced rule. Deferred rather than absorbed because a round-12 test-only push re-arms every lane on a diff that is otherwise converged; the guard hardens against FUTURE join sites, not any current gap — all 11 present sites are converted and pinned by the regression tests in this PR.
The tokenizer/canonicalization architecture decision now has an owner and an issue as this Watch item asks: #9171 (assigned, Due 2026-10-05), recording the open questions (not-yet-existing write targets, non-Windows hosts) and the residuals a tokenizer would subsume.

@NicholasRBowers

Copy link
Copy Markdown
Contributor Author

self-added: no

  • fixed — Watch: the 64→254 cap raise and the hook-path liveness restructure ship undeclared in the description

Both are now declared in the PR body's Fix section (edited in place, no code change): the win_gsep excursion-segment cap raise {1,64}{0,254} with its pad-past-cap rationale, and the hook-path liveness restructure with the measured ~56 s backtracking figure and the tests that pin it (test_security_gate_liveness.py, resized flood tests). A human merging on the description alone now sees both.
The second Watch item (the _BARE_TOKEN_PROTECTED_LEAVES residual is real and correctly counted) is confirmatory — that residual stays declared in the body and is additionally in scope of the tokenizer decision now tracked in #9171.

@bolichen97

Copy link
Copy Markdown
Collaborator

Closing as superseded on main by merged #9183 (with #9089).

This PR extends the command-text path fence in src/kiro_crew/security.py (trailing-dot and 8.3 short-name spellings). main has since deleted that layer entirely -- #9183's message: the text layers that duplicated the sandbox are "deleted, not narrowed"; is_sensitive_bash_command keeps only the size ceiling, IMDS and env-credential checks, and test/test_security.py now carries test_the_path_matchers_are_absent asserting not hasattr(security, name) for exactly the helpers this diff patches. The diff has no landing site.

The Windows-spelling concern itself is still valid and should be re-triaged on the new architecture (is_sensitive_path's bounded resolve plus the sandbox bind-mask): issue #5265 stays open for that, and the follow-ups this PR filed (#8128 shrinking-stem 8.3 tails, #9170 join-site guard, #9171 tokenizer) remain the right place to track it. Thanks, Nicholas.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merge conflict Branch has merge conflicts with its base — author must resolve before merge needs-human PR flagged for human review by drive-to-green pipeline

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Windows path-normalization forms bypass the shell credential fence (trailing dot, 8.3 short name)

5 participants