Skip to content

feat: security hardening — URL secret redaction + bounded error-body reads#63

Merged
lesnik512 merged 12 commits into
mainfrom
feat/security-hardening
Jun 14, 2026
Merged

feat: security hardening — URL secret redaction + bounded error-body reads#63
lesnik512 merged 12 commits into
mainfrom
feat/security-hardening

Conversation

@lesnik512

Copy link
Copy Markdown
Member

Summary

Closes the security cluster from the 2026-06-14 deep audit — the dimensions (security/supply-chain) the prior audit never covered. Design + plan: planning/changes/active/2026-06-14.03-security-hardening/.

1. URL secret redaction (3 leakage findings). New _internal/redaction.py::redact_url strips user:pass@ userinfo and masks the values of known-sensitive query and fragment keys (case-insensitive, whitespace-tolerant). Wired into:

  • StatusError._summary / __repr__ (replaces the userinfo-only _strip_userinfo)
  • every resilience-middleware event URL, via a single _observed_url(request) helper (so a new emit site can't silently re-leak)

Benign URLs (no sensitive key) pass through byte-identical.

2. Bounded error-body read (streaming finding). New public ResponseTooLargeError + opt-in max_error_body_bytes: int | None = None on both AsyncClient and Client. When set, stream() raises on a 4xx/5xx whose declared Content-Length exceeds the cap before reading the body. None default = unchanged behavior. Honest residual (documented): a chunked error body with no declared length is still read, because a true mid-read cap would require httpx2's private _content (banned by repo invariant). The non-streaming hard cap is recorded in planning/deferred.md.

3. Docs. trust_env proxy default, the bounded-body residual, and a StatusError.response.request header-reachability callout (Authorization/Cookie are not stripped — redact before logging).

Also: a small coverage-config improvement (exclude pytest.fail() lines globally instead of per-line pragmas).

Test plan

  • redact_url unit tests: userinfo (incl. IPv6/port), each sensitive key, fragment, whitespace-padded keys, repeated keys, benign byte-identical passthrough
  • Leakage tests assert both secret-absence and the REDACTED form — for StatusError messages and resilience events (sync + async)
  • Bounded-read tests: over-cap raises ResponseTooLargeError without reading; under-cap / no-cap still populate exc.response.content (sync + async); _parse_content_length is total
  • ResponseTooLargeError pickle round-trip + public-API export
  • just test — 602 passed, 100% coverage
  • just lint — ruff + ty clean
  • Invariants: no httpx2._, no raw str(request.url) in middleware, no print(), no from __future__

🤖 Generated with Claude Code

lesnik512 and others added 12 commits June 14, 2026 15:52
…body)

Spec for the deep-audit security cluster: URL secret redaction (known
sensitive query keys) across logs/telemetry/errors, an opt-in
Content-Length-gated bound on the stream() error-body pre-read with a new
ResponseTooLargeError, and trust_env docs. Non-streaming hard body cap
deferred.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
7-task TDD plan: redact_url sanitizer, StatusError + middleware rewiring,
ResponseTooLargeError, opt-in max_error_body_bytes with bounded stream()
pre-read, docs, and full verification.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… keys)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Closes two Minor leak paths from code review: redact_url now masks
sensitive keys in the fragment as well as the query, and matches keys
after stripping surrounding whitespace.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…re-read

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The new bounded-error-body tests raise on stream enter, so the `async with`
body never runs. Match the file's existing idiom (pytest.fail(...) #
pragma: no cover) to restore 100% line coverage.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
pytest.fail(...) marks a branch that must never execute, so exclude it
globally in coverage.report.exclude_also instead of per-line pragmas. Drops
the # pragma: no cover comments added for the bounded-stream tests, and
strengthens the whitespace-padded-key redaction test to assert the masked
form. Pre-existing pragmas elsewhere are now redundant and can be swept
separately.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Move URL redaction from per-emit-site _observed_url() into _emit_event
itself, so the single emission boundary sanitizes the `url` attribute for
both the log record and the OTel span event. A future emit site can no
longer reintroduce the leak by passing a raw url, and the four middleware
modules revert to their original `str(request.url)` (net-unchanged vs main).

Adds direct _emit_event redaction tests for both channels.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@lesnik512 lesnik512 merged commit 3adfd9a into main Jun 14, 2026
5 checks passed
@lesnik512 lesnik512 deleted the feat/security-hardening branch June 14, 2026 13:36
lesnik512 added a commit that referenced this pull request Jun 14, 2026
Promote 2026-06-14.02-pydantic-import-isolation (#62) and
2026-06-14.03-security-hardening (#63) to changes/archive/ with
status: shipped, pr, and outcome filled; move their Index lines from
Active to Archived. The deep-audit umbrella bundle stays active (still
spawning remediation).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant