fix(security): fence repeated Windows path separators (#6350) - #6993
Conversation
Design Review (Fable 5) — 🟡 CONCERNSDesign-level review of Design-Verdict: CONCERNS Sound fix (canonicalize the subject once, linearly), but the PR description documents the rejected pattern-widening approach, not the code that shipped. Watch
[DESIGN-REVIEWED] b0f2bd0 |
First Principles Review (Fable 5) — 🟡 CONCERNSPremise-level review of All three raw-text matchers are consumed only inside First-Principles-Verdict: CONCERNS The fix is cause-level and every piece earns its place, but the description narrates a pattern-level fix the diff explicitly rejected as a DoS. What this change shipsIntent: stop a doubled Windows path separator from slipping past the credential/write fence (#6350). This is a FIX.
No new public surface: all three added names are module-private with exactly one consumer each ( Watch
[FIRST-PRINCIPLES-REVIEWED] b0f2bd0 |
Opus 4.8 Review — ✅ no blocking findingsReviewed Review detailsNo blocking issues — one advisory over-block regression in the new separator-collapse pass. FINDING — src/kiro_crew/security.py:6883 — because [OPUS-REVIEWED] b0f2bd0 Verdict parsed from the review's SHA-scoped output markers for commit False positive or not applicable? A repository writer can comment: |
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: |
bolichen97
left a comment
There was a problem hiding this comment.
Readiness passed on the current head, all checks green, mergeable against main, and no outstanding change requests from any reviewer. Approving as part of a maintainer sweep of fix-type PRs.
d7c339d to
e9013c4
Compare
e9013c4 to
3a4e310
Compare
|
The rebase onto main brought a new BLOCKING finding, and it is REAL. Fixed in BLOCKING -- separator runs can crash the gateway. ACCEPTED, REPRODUCED, and the mechanism was mine. I measured it rather than arguing from the earlier round's numbers, because the rebase moved the surrounding code:
So 6,000 backslashes crossed the 25s watchdog exactly as reported. My earlier comment claiming the disjoint character classes kept this linear was wrong: disjointness bounds the NAME run, not the separator run. Two fixes were tried and rejected, both measured:
Shipped fix: collapse the run in the SUBJECT, not the patterns.
One test moved with the design: Verified on Note for the maintainer: the rebase force-push dismissed the earlier approval, so this needs re-approving before it can merge. |
|
CI note, so the red on this PR is not misread as the fix regressing something. The failing backend shards are broken on main, not caused by this branch. Reproduced on pristine
The remaining red, Correcting my own earlier evidence on this thread: I previously said both files "pass locally on this branch", offering that as proof the failures were not mine. That reasoning was wrong even though the conclusion held. This worktree predates main's addition of No push is being made to chase these: the |
|
Settled, with the clean comparison I should have reached for first: main's own CI is red with exactly this PR's failure set. Run That is the same five checks, same shard numbers, as here. And the Windows shard 3 failure on main (job So both failures are base-owned:
Two corrections to my earlier reasoning on this thread, since both were methodologically wrong even where the conclusion held:
The check that actually settles ownership is the one above -- main's own CI on main's own commit. No cross-PR inference, no local worktree that might not match main's tree. Nothing here is pushed: the |
Pull request was closed
Win32 collapses a repeated path separator, so `%LOCALAPPDATA%\\kiro-cli` and `%LOCALAPPDATA%\kiro-cli` name the same entry. The shell credential fence matched raw text with exactly ONE separator per boundary, so a doubled separator at an inter-segment boundary reached the fenced store while matching no branch: the live SSO bearer-token database under `AppData\Local\kiro-cli` auto-approved as a read-only shell read. The cause is one shared definition. `win_gsep` (the generalized separator consumed by the fenced-dirs pattern, the home-anchored path, the AppData alias remainders, the write-protected prefixes/leaves and the agents dir) ended in exactly one `win_sep`, and its repeat group accepted only `\.` or `NAME\..` excursions, never a bare extra separator. Fixed at that definition plus the other sites that spelled a single separator directly: - `win_seps` (one-or-more) replaces the single separator at every join site INSIDE a path; the trailing boundary stays single, since it only has to see that the path ended. - the generic drive-letter home anchor (`C:\Users\u`), - the resolved home literal, rendered through the run instead of re.escape()d whole, so a home outside Users/home (`D:\profiles\u`) is anchored too, - the %APPDATA% / %LOCALAPPDATA% anchor-specific `\..\Roaming|Local` excursions, - the crew variable-leaf join, - the anchorless relative-traversal matcher and its segment alternation. Measured against the base with a 34-row matrix: 22 fenced spellings were permitted before and none is now, with zero benign paths newly refused. Two rows go beyond the report: `.aws` and `.ssh` DO leak (the report doubled the separator before the leaf, which the trailing boundary already absorbs, rather than after the anchor), and so does the `~/.kiro/agents` write gate, which is a code-execution boundary rather than a read fence. The added run classes are disjoint from the name run (which excludes separators) and from `.`, so no new quantifier ambiguity is introduced; a pathological-input test pins the decision time. Closes #6350
ced7748 to
b0f2bd0
Compare
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. |
What is the problem?
Win32 collapses a repeated path separator, so
%LOCALAPPDATA%\\kiro-cliand%LOCALAPPDATA%\kiro-cliare the same directory. The shell-side credentialfence (
is_sensitive_bash_command) matches raw command text and acceptedexactly ONE separator per boundary, so a doubled separator anywhere in the
chain named the fenced store while matching no branch of the matcher:
type "%LOCALAPPDATA%\kiro-cli\data.sqlite3"type "%LOCALAPPDATA%\\kiro-cli\data.sqlite3"type "%LOCALAPPDATA%\\\kiro-cli\data.sqlite3"type "%LOCALAPPDATA%//kiro-cli/data.sqlite3"type "C:\Users\me\AppData\\Local\kiro-cli\data.sqlite3"cat "C:\Users\me\\.aws\credentials"echo x > "C:\Users\me\.kiro\\agents\evil.json"Two of those rows go beyond what #6350 reported, and both matter for how the
issue is sized:
.awsand.sshdo leak. The report concluded they stay blocked, but itonly doubled the separator before the leaf, which the trailing boundary
already absorbs. Doubled after the anchor they were permitted.
~/.kiro/agentswrite gate leaks too. That gate is a code-executionboundary, not a read fence: a spec planted there becomes a
KIROCREW_MCP_TARGET_<SERVER>command the gateway execs outside theper-session sandbox.
Measured on the base commit with a 34-row matrix: 22 fenced spellings were
permitted, 0 benign paths refused.
Why this issue matters to the user
The fenced directories hold kiro-cli's live SSO bearer token, and this gate is
what stops read-only shell auto-approval and the agent file tools from reaching
them. An agent that reads
data.sqlite3holds that token. The bypass needs nounusual privilege and no symlink, only an extra backslash in a path the agent
was already going to write, and the doubled spelling is not one a human reviewer
reads as suspicious. For the agents-dir row the consequence is a persistent
unsandboxed command running as the user rather than a credential read.
How our fix solves it
Symptom: a doubled separator at an inter-segment boundary is permitted while
the single-separator spelling of the same file is blocked.
Mechanism:
win_gsep, the generalized separator, ended in exactly onewin_sep, and its repeat group accepted only the canonical no-ops\.andNAME\... A bare extra separator matches neither, so the chain simply failedone segment in. A doubled separator immediately before the leaf still blocked,
because the trailing boundary
(?:{win_sep}|\s|$|['"])absorbs one -- which iswhy the gap looked narrower than it was.
Root cause: the separator was defined as "exactly one" in one shared place.
win_gsepis consumed by the fenced-dirs pattern, the home-anchored path, the%APPDATA%/%LOCALAPPDATA%remainders, the write-protected prefixes andleaves, the crew variable-leaf parents and the agents dir, so every store branch
inherited the same hole at once. This is fixed at that definition rather than
per spelling:
win_seps(one-or-more) replaces the single separator at every join siteinside a path. The trailing boundary deliberately stays single: it
only has to observe that the path ended, and the rest of a run is text it
never consumes.
win_gsepare moved over too, or the class re-enters one segment to the side:the generic drive-letter home anchor (
C:\\Users\\u), the resolved homeliteral, the
%APPDATA%/%LOCALAPPDATA%anchor-specific\..\Roamingand\..\Localexcursions, the crew variable-leaf join, and the anchorlessrelative-traversal matcher with its segment alternation.
re.escape()d whole, so a run inside the anchoritself escaped it. It is now rendered through the run.
generic_win_homealready covers the
Users/homeshapes; a home elsewhere(
D:\profiles\u) has only this literal to match on.Over-matching is the safe direction for this gate (naming a fenced path is the
signal), but this change also touches the write-protection gate, where
over-matching would refuse a legitimate write. That is why every run shape is
paired with benign controls in the tests.
On backtracking: the two separator classes are disjoint from the name run
(
[^\\/\s'\"]{1,64}, which excludes separators) and from., so admittingone-or-more introduces no new quantifier ambiguity, and the name run keeps its
length cap. A pathological-input test pins the decision time.
What tests we did
New
TestWindowsSeparatorRunsintest/test_security.py(134 cases),asserted as a matrix -- every anchor x every run shape x every boundary
position -- because closing these one spelling at a time is what produced the
current shape:
C:\Users\u,%USERPROFILE%,$env:USERPROFILE,~,$HOME) for.awsand.ssh;parametrized off
_SENSITIVE_HOME_DIRSso the matrix cannot drift from thelist;
%APPDATA%/%LOCALAPPDATA%branches including their own excursions;\.,X\..);$env:KIRO_HOME, the crew variable leaf, andevery entry of
_WRITE_PROTECTED_BASH_LEAVES;_build_sensitive_regex()under a patched non-
Usershome -- a pure function of the home, so no modulecache is disturbed;
.awsxandagentsx(a name that merely starts with a fenced one) and an ordinary project path
with a doubled separator;
Mutation-verified against the base commit: 131 of the 134 fail without the
security.pychange. The 3 that pass on base are the two guards that aresupposed to (the benign controls and the timing bound) plus one leaf
parametrization that a separate anchorless name rule already covered.
Gates run:
pytest test/test_security.py(1129 passed, 1 skipped),test_hooks.py,test_governance_self_protection.py,test_connections_tool_aliases.py,test_app_sources_write_protection.py,test_app_registry_source_seams.py(626 passed),test_trust_reads.py,test_mcp_cron_security.py,test_computer_use_api.py,test_aws_consent.py,test_workflows_library.py(458 passed). flake8, isort, mypy and black clean onboth touched files. Decision time on the pathological inputs is unchanged or
better than base (two of the four 4KB probes drop from ~750ms to ~0.3ms because
they now match early instead of scanning to the end).
What is NOT verified, stated plainly because this is a security control:
there is no Windows host in this environment. Nothing here observed Win32
actually collapsing a separator run; that equivalence is taken from the platform
contract, and what these tests pin is the matcher's side of it. That is sound
for this change because the fence is a pure text function -- the raw pass
never reads
os.name, so every assertion above runs and means the same thing onthe Linux, macOS and Windows CI shards. No filesystem resolution is involved,
which is exactly what distinguishes this class from the 8.3 short-name class in
#5265.
Any other suggestions on the work
code span and Repeated path separator bypasses the Windows kiro-cli credential fence #6350 suggests handling them together, but Windows path-normalization forms bypass the shell credential fence (trailing dot, 8.3 short name) #5265 already has an
open PR (fix(security): fence trailing-dot and 8.3 Windows spellings (#5265) #5336) that rewrites the same
win_gsepstatement for thetrailing-dot and 8.3 classes. Doing both here would have duplicated live work.
The two changes are complementary and the textual overlap is one statement, so
whichever lands second rebases mechanically; fix(security): fence trailing-dot and 8.3 Windows spellings (#5265) #5336's
win_gsepstill ends inexactly one separator, so it does not close this class on its own (verified
against its head
d19f8141).path-shaped tokens and compare the resolved path against the fenced set.
Worth noting that most of that engine already exists on
main--_shape_path_tokencollapses separator runs, strips trailing dots and spaces,and nets
..-- but it is only wired into the pass-3 cwd-walk scan. Reusing itfor the whole fence is the real end state and is an architecture change to a
security boundary, so it wants its own design ticket rather than riding along
here.
{1,64}cap on the excursion name run is untouched by this PR and isdefeatable by a 65-character segment. It is pre-existing and is being addressed
in fix(security): fence trailing-dot and 8.3 Windows spellings (#5265) #5336.
Closes #6350