fix(auto_improvement): redact pip stderr before bounding in install_deps (#7307) - #7316
Conversation
install_deps() returned pip's stderr tail line to the caller's error payload unredacted and bound-before-redact. The child pip inherits the gateway environment, so an authenticated private index reaches it; on an auth failure pip echoes the raw request URL (token included) to stderr, which then surfaced in the dashboard. Pass the full tail line through security.redact_and_truncate(.., 200) so redaction runs over the whole string before the 200-char bound, matching the redact-before-bound invariant. Same fix shape as handlers/memory.py (#7283). Add regression tests pinning that a credential-bearing stderr line, and one straddling the truncation boundary, never reach the payload. Fixes #7307
Design Review (Fable 5) — ✅ PASSDesign-level review of Verified: Design-Verdict: PASS Root-cause fix at the source using the existing documented invariant helper, pinned by a boundary-straddling regression test — nothing to redesign. [DESIGN-REVIEWED] 202e978 |
First Principles Review (Fable 5) — 🟡 CONCERNSPremise-level review of All claims verified. The fix reuses the existing First-Principles-Verdict: CONCERNS Mechanism-level fix reusing the right helper — but it's the second point patch in a series with 12 counted slice-first stderr siblings left in this app. What this change shipsIntent: stop a pip index credential leaking (whole or as a boundary-cut fragment) into the dashboard's install-failure message — a FIX.
Watch
Subtractions
[FIRST-PRINCIPLES-REVIEWED] 202e978 |
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: |
|
🤖 Kiro Crew Auto-Pipeline [operator: bolichen97#bb3ad1ca]: This issue was claimed by this pipeline instance before the PR appeared, so rather than opening a duplicate I adopted this PR and pushed a repair commit ( |
UX Review (Fable 5) — ⏭️ skippedRevision |
Repairs the first revision of this PR (kiro-agent): - Register deps.py in NON_EGRESS_REDACTION_MODULES: the posture drift-guard (test_every_redactor_call_site_is_a_registered_sink_or_allowlisted) requires every redactor call site to be classified, and it red both Linux and Windows backend shards. deps.py is a source-side pre-pass, not an egress boundary: the payload is served only through routes.py, the registered sink. - Repair the straddle regression test: the prior layout put the token start at index 202, entirely past the 200-char bound, so the test passed even on the unfixed code. The token now starts at index 190 with a premise guard (assert start < 200 < start + len(token)), and the exact prefix fragment a bound-before-redact implementation leaks is asserted absent. Mutation-verified: raw slice fails both tests, slice-then-redact reorder fails the straddle test. - Split the over-long return line (black line-length gate) and make the code comment precise: the serving route does redact, but a mid-token cut breaks the credential-regex match, which is the reachable defect. Co-authored-by: Kiro Crew <noreply@kiro.dev>
9c962f7 to
202e978
Compare
|
🤖 Kiro Crew Auto-Pipeline [operator: bolichen97#bb3ad1ca]: Answering the First Principles 🟡 CONCERNS (advisory) on Watch item (12 unfixed siblings, second point patch after #7283): Accepted — and acted on beyond deferral: the census is verified and tracked in #7333, now updated with every site FP counted plus one more both reviews missed ( Subtraction (shrink the 8-line comment): Declined with rationale — the comment carries the one fact that is site-specific and NOT in |
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. |
Closes #7307.
Problem
install_deps()insrc/kiro_crew/apps/builtins/auto_improvement/backend/deps.pyreturned pip's stderr tail line in the handler'serrorpayload with two defects:PIP_INDEX_URL) echoes its request URL — token and all — to stderr on an auth failure.tail[0][:200]sliced first. This is the defect that stays reachable end to end: the serving route (_handle_deps_installinroutes.py) does redact what it sends, but its credential regexes need the full credential shape to match. A token straddling the 200-char boundary is cut mid-match (e.g.https://ci-bot:TOKENedg, no trailing@host), so the fragment sails through the route'sredact()and renders in the dashboard. Verified empirically: the sliced fragment survivesredact()on the old code; the issue's stronger claim that the payload reaches the UI with no redaction pass at all does not hold (the route is fail-closed), but the partial-fragment leak does.This is the same mechanism fixed for
handlers/memory.pyin #7283 (issue #7279);deps.pywas the only remaining pip-stderr site with the slice-first shape (it evaded thestderr.decodegrep because it usestext=True).Fix
redact_and_truncate(tail[0], 200)apply the bound — the redact-before-bound invariant documented onredact_and_truncateitself (security.py). No manual slice remains anywhere in the expression.from kiro_crew.security import …convention used by sibling backend modules (commit.py, mcp_server.py, routes.py). No import cycle: the module imports cleanly at module scope.apps/builtins/auto_improvement/backend/deps.pyinNON_EGRESS_REDACTION_MODULES(security_posture.py): it is a source-side pre-pass, not an egress boundary — the payload reaches the dashboard only throughroutes.py, the registered sink for this app. This is what the posture drift-guard (test_every_redactor_call_site_is_a_registered_sink_or_allowlisted) requires of every new redactor call site, and it is the test that failed on the first revision of this PR.Scope note (exception branch): the
install failed: {exc}branch two lines earlier is left unchanged deliberately. AnOSError/SubprocessErrorstring is a spawn failure (strerror, argv); pip credentials ride the environment (PIP_INDEX_URL), never argv, so that string cannot carry the index token, and the route's fail-closedredact()already covers it as defense in depth. Wrapping it would widen the diff without a reachable defect behind it.Tests
Two regression tests in
TestInstallDeps(test_backend_deps_cov80.py), stubbingsubprocess.run(no real network/pip):test_a_pip_failure_does_not_leak_index_credentials_to_the_payload— a credential-bearing URL in stderr must not survive intoerror(pins defect 1 at the source).test_a_pip_failure_credential_is_redacted_before_it_is_bounded— lays the token out to start at index 190 so the 200-char bound cuts ten characters into it, asserts the exact prefix fragment a bound-before-redact implementation would leak (token[:10]) is absent, and carries a premise guard (assert start < 200 < start + len(token)) so the test can never silently stop straddling the boundary. The first revision's version of this test padded the token to start at index 202 — entirely past the bound — so it passed even on the unfixed code; this revision repairs the layout and locks the premise into the test itself.Mutation-verified both ways: reverting the fix to the raw slice (
tail[0][:200]) fails both tests; reordering to slice-then-redact (redact_and_truncate(tail[0][:200], 200)) fails the straddle test. Restored code passes.Verification
python -m pytest77379 passed / 339 skipped; 96 failures + 2 errors, all in the known host-environment baseline classes (AF_UNIX path length, sandbox contention), zero in the touched areas — verified by failure-file histogram.isort 6.0.0/flake8 7.1.0(0 findings) /mypy 1.14.1(no issues in 1216 files) /scripts/check_black_formatting.py(CI-identical two-direction gate) all pass.test/test_security_posture.py42 passed (including the drift-guard that failed on the first revision).Pattern harvest
Rule candidate: semgrep — flag a subscript slice (
x[:N]) applied tosubprocessstderr/stdout that is formatted into a returned or served payload string unless the expression is wrapped byredact_and_truncate(redact-before-bound invariant). Thesecurity_posturedrift-guard already harvests the sibling half of this class: every new redactor call site must be classified as sink or non-egress, so an unregistered fix like this one cannot land silently.