fix(security): cronScript auth header + OAuth state entropy fence - #8263
Conversation
Design Review (Fable 5) — ✅ PASSDesign-level review of Design-Verdict: PASS A verified banner bypass, closed by bounding the exemption to protocol-emittable shapes rather than reverting #8051's false-positive fix — the right middle path. The charset/length fence is well-derived (base64url can't emit Suggestions
[DESIGN-REVIEWED] 9f2c6bf |
UX Review (Fable 5) — ✅ PASSUX-level review of This PR is almost entirely backend security-heuristic hardening (OAuth entropy shape fence in UX-Verdict: PASS No user-facing surface changes; the one frontend line fixes cron-script fetches to authenticate like its siblings, unbreaking that view. [UX-REVIEWED] 9f2c6bf |
First Principles Review (Fable 5) — 🟡 CONCERNSPremise-level review of All checks done. Composing the review. First-Principles-Verdict: CONCERNS F2 is a cause-level fix that earns every line; F1's premise — "the one call that did not" — is false, and the blessed What this change shipsIntent: restore the operator's credential warning on OAuth consent URLs and the session-key header on the cron-script read — a FIX (two declared fixes, nothing rides along).
Watch
Subtractions
[FIRST-PRINCIPLES-REVIEWED] 9f2c6bf |
Opus 4.8 Review — ✅ no blocking findingsReviewed 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: |
16cdf9d to
165f8b6
Compare
Confirmed by running the payload, not by argument. The other lane rebutted this same mechanism on the grounds that
Taken as written. One correction to the finding's stated outcome, because the PR body must not claim more than it delivers: refusing the exemption does not by itself make the banner warn on a doubly-encoded secret. The markerless scan evaluates only the raw URL plus one |
165f8b6 to
9f2c6bf
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. |
Problem / Motivation
Two verified security findings from the watchdog queue.
F1 —
cronScriptomitted the session-key header. Inwebsite/src/api/client.ts, every/api/crons/*call passes{ headers: { ..._sk } };cronScriptwas the one that did not.F2 (#8051) — the OAuth front-channel entropy exemption had no shape bound. Commit 1b48454 (
fix: scope OAuth entropy exemption to approved parameters) replaced a guarded predicate with an unconditional one:It dropped
_PKCE_S256_CHALLENGE_RE([A-Za-z0-9_-]{43}\Z), thecode_challenge_method == ["S256"]requirement, and both_contains_fixed_credential/_text_contains_bare_secretguards, and widened the exempt set fromcode_challengealone tostate,nonce, andcode_challenge. Because_BARE_SECRET_RUN_REis[A-Za-z0-9+/]{40,}— the AWS-secret shape — a raw AWS secret instate=at an approved endpoint was blanked before the markerless scan ran, sooauth_url_contains_credential()returnedFalseand the operator approval banner showed no credential warning. It also deletedtest_bare_aws_secret_inside_state_fails_closed_everywhereandtest_pkce_challenge_wrapping_bare_aws_secret_fails_closed.Why it matters
The approval banner is the operator's last look at a consent URL before authorising it. A credential smuggled into
state=at an approved endpoint reached that banner with no warning, so the one human check in the flow was silently disarmed for exactly the shape the heuristic exists to catch.F1 is the weaker of the two and this PR does not overclaim it: it is not exploitable at the handler today (see Manual verification). It restores an invariant the file documents at its own
_skdefinition, so a gate added to this read later does not silently skip.What changed (motivation → approach → change)
F1 (one line). Added
{ headers: { ..._sk } }tocronScript, copied verbatim from the siblingcronRunDetailcall directly above it.F2 — the charset/length bound, deliberately not the
_text_contains_bare_secretrestore. Restoring that guard is the smaller edit but is a straight revert of #8051's real bug fix: its own retained testtest_recognized_oauth_entropy_does_not_hit_bare_secret_lotteryasserts_text_contains_bare_secret(entropy) is Truefor a legitimate S256 challenge and a legitimate 40-charstate, so that guard is precisely what produced false credential banners on real sign-in URLs. The exemption is instead bounded to the shapes the protocol itself can emit:-/_and never+or/, so a value containing either cannot be protocol entropy and keeps the markerless scan;code_challengeis base64url of a 32-byte digest, so it is exactly 43 characters; any other length in that field is not a challenge shape;_exfil_url_warning:%252Fdecodes to%2F, which still carries no literal/, so a raw-plus-one-decode test would hand the exemption to a base64-standard AWS-secret run. The predicate decodes until the text stops changing, bounded by the module's existing_MAX_URL_DECODE_PASSES, and fails closed when a layer still remains at the bound. This mirrors the established idiom at_exfil_url_warningrather than inventing a second one.The endpoint gate, the fixed-credential passes, and the diagnostic contract are untouched.
Two residuals, both narrowed in the spec rather than left implied:
stateentropy and is still banner-exempt at an approved endpoint. Unavoidable without reintroducing the false positive; roughly 72% of random AWS secret keys contain a+or/and are now caught, and the general output redactors keep the heuristic for all of them regardless.state— a provider that base64-standard-encodesstate(percent-encoded%2F/%2B) will get a banner warning it did not get before. That is the deliberate direction: the two shapes are indistinguishable, and fix: scope OAuth entropy exemption to approved parameters #8051's own "legitimate entropy" fixture is standard-alphabetbase64.b64encode(digest)[:40], passing only because that particular digest happened to contain no+or/.Tests
test_bare_aws_secret_inside_state_fails_closed_everywhere(the/-bearingBARE_AWS_SECRET, which the charset bound catches).test_percent_encoded_secret_alphabet_cannot_buy_the_exemption— single-encoded%2Fis flagged end to end.test_no_encoding_depth_earns_the_entropy_exemption(single / double / triple / over-budget) — no encoding depth wins the exemption. Deliberately scoped to the predicate, with a comment saying so, because whether the banner then warns on a doubly-encoded run is a separate pre-existing property (see Manual verification) and this test must not appear to cover it.test_off_length_challenge_loses_the_s256_exemption— a 40-char value incode_challengeis not a challenge shape.test_markerless_secret_shape_is_banner_exempt_but_generically_redacted'sstatecase now uses the alphanumeric secret rather than the/-bearing one, so it documents residual 1 instead of contradicting the restored test.test_pkce_challenge_wrapping_bare_aws_secret_fails_closedwas not restored: its fixture is a 43-char purely-alphanumeric value, indistinguishable from a real S256 challenge by shape. It falls inside residual 1 and no shape bound can catch it.test/test_security.py: 1295 passed, 1 skipped.Revert-verify. Reverting
src/kiro_crew/security.pyalone, leaving the tests: 7 failed without the fix, 7 pass with it — so each new test is load-bearing rather than passing incidentally.Manual verification
The
%252Fbanner blind spot is pre-existing and is NOT closed here. After this fix the exemption is correctly refused for a doubly-encoded secret, but the banner still does not warn, because the markerless bare-secret scan evaluates only the raw URL plus oneunquotewhile_MAX_URL_DECODE_PASSESis 3. That is a property of the scan, not of the exemption, and it reproduces outside every OAuth exemption:%252Fis equally unflagged in a parameter that was never exempt (&session_blob=) and at a completely unapproved endpoint (https://attacker.example/x?blob=), where no exemption code runs at all — on this branch and on the base alike. Closing it means deepening the scan loop for every parameter and every endpoint, which is a wider change than this PR's purpose; flagged for a maintainer rather than silently widened here.F1 is not exploitable at the handler today.
api_cron_script_sourceinsrc/kiro_crew/dashboard/handlers/cron.pynever reads the session key, the route is already authenticated by the route-widetoken_auth_middleware, the job id is the only caller-supplied input, and the file path is derived server-side from the stored job record.Depth × parameter matrix, run against the built module: no encoding depth (literal,
%2F,%252F,%2525…) wins the exemption for a/- or+-bearing secret instate,nonce, orcode_challenge; and every legitimate fixture (opaque-state-123, a 40-char base64url state/nonce, both 43-char S256 challenges) stays exempt.Screenshots / video
Why no screenshot: the only frontend change adds a request header argument to one API method; nothing rendered changes.
Related Issues
no linked issue: filed from the security watchdog queue rather than a tracked issue; #8051 is named in the body as the commit under repair, not as an issue to close.
Pattern harvest
Rule candidate: review-prompt
Pattern: a security predicate that judges a URL-derived value on the raw plus one decoded form, in a module that already establishes decode-until-stable bounded by
_MAX_URL_DECODE_PASSES. Both defects in this PR are the same shape — an exemption widened without a bound on what it exempts — and the second was found only because a reviewer probed encoding depth. Any new "this value is safe to skip scanning" predicate should be required to state its decode depth explicitly.Checklist