Skip to content

fix(core): validate User-Agent up front and stop unwrapping header values - #152

Open
alukach wants to merge 4 commits into
mainfrom
fix/header-values
Open

alukach wants to merge 4 commits into
mainfrom
fix/header-values

Conversation

@alukach

@alukach alukach commented Sep 25, 2026

Copy link
Copy Markdown
Member

What I'm changing

ProxyGateway::with_user_agent accepted any string and stored it unchecked. Every forwarded request then ran self.user_agent.parse().unwrap(), so a value containing a control character (a stray newline from an env var, say) panicked on the first backend request rather than failing when the gateway was configured. The same parse().unwrap() pattern appeared on 10 constant content-type headers and on the runtime-built SigV4 headers in the outbound request signer, where a configured session token with a bad byte would also panic mid-request.

This PR moves validation to configuration time, uses HeaderValue::from_static for constants, and makes the signer's header construction return an error instead of panicking.

Stacked on #151 so the new test lands in proxy/tests.rs.

How I did it

  • ProxyGateway (crates/core/src/proxy.rs): the user_agent field is now a HeaderValue. with_user_agent takes impl TryInto<HeaderValue> and returns Result<Self, ProxyError>, mapping an invalid value to ConfigError. The five per-request insert sites became self.user_agent.clone(). This is a signature change for integrators: add ? after the call (docs updated in docs/architecture/request-lifecycle.md).
  • ProxyResult::xml/json (route_handler.rs): HeaderValue::from_static. The hand-built XML responses in proxy.rs (ListBuckets, list, DeleteObjects result, error_response) and the SlowDown responses in crates/metering and examples/cf-workers/src/rate_limit.rs now call ProxyResult::xml instead of assembling the header map themselves.
  • S3RequestSigner::sign_request and sign_s3_request (backend/request_signer.rs, backend/multipart.rs): a pub(crate) fn header_value(name, value) maps HeaderValue::from_str failures to ProxyError::Internal. The message names the header but deliberately omits the value, since it may be a credential. The canonical-headers loop drops its headers.get(k).unwrap() in favor of and_then(to_str).unwrap_or("").
  • examples/cf-workers/src/lib.rs: one-line redundant_closure fix that cargo clippy --fix applied on the wasm target while I was in there.

Test written first (invalid_user_agent_is_rejected_at_configuration), confirmed it did not compile against the old API, then fixed.

Test plan

  • cargo test — 159 core unit tests pass, all other crates green
  • cargo clippy -- -D warnings
  • cargo check -p multistore-cf-workers --target wasm32-unknown-unknown
  • cargo check -p multistore-cf-workers-example --target wasm32-unknown-unknown
  • cargo fmt --check

🤖 Generated with Claude Code

@claude

claude Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

Claude finished @alukach's task in 3m 18s —— View job


Claude finished reviewing this PR

  • Gather context (fetch diff, read CLAUDE.md)
  • Review diff for correctness/security/best-practice issues
  • Run ponytail-review pass for over-engineering
  • Post final review

❌ Changes requested — see findings below.

  • crates/oidc-provider/src/backend_auth.rs:104 — does not compile. The if let Some(ref bucket_config) = ctx.bucket_config pattern was changed to if let Some(bucket_config) = ctx.bucket_config.take(), which moves bucket_config out as an owned Cow<'_, BucketConfig> instead of a reference. resolve_aws still takes &BucketConfig, so self.resolve_aws(bucket_config) fails with E0308: expected &BucketConfig, found Cow<'_, BucketConfig>. Confirmed live on this PR's CI — Cargo Check, Clippy, Unit Tests, and Integration Tests are all failing on exactly this error (workflow run 36158463212).

    let options = self.resolve_aws(&bucket_config).await?;

    (matches the compiler's own suggestion) This hunk is unrelated to the PR's stated header-validation scope — looks like it crept in from the same cleanup pass as the other unwrap() removals — but it needs to compile either way.

No other correctness or security issues found in the diff. The core change itself is sound:

  • ProxyGateway::with_user_agent validating at configuration time via TryInto<HeaderValue> and returning ProxyError::ConfigError is the right fix — moves failure from first-request-panic to startup.
  • header_value() in request_signer.rs correctly omits the value from its error message (may be a credential), and the canonical-headers loop's unwrap() → and_then(...).unwrap_or("") is a no-op behavior-wise (it was already falling back to "" on a bad to_str(), just via a different unwrap chain), so no signing-behavior regression there.
  • ProxyResult::xml/json switching to HeaderValue::from_static and callers reusing them instead of hand-rolling HeaderMap is a clean dedup.

Simplify (ponytail)

Nothing to cut — header_value() and the lock() poison-recovery helper in jwks.rs are each used multiple times and directly replace repeated unwrap() boilerplate, not speculative abstraction.


💰 Estimated review cost: $0.97 · 3m17s · 36 turns

@github-actions

github-actions Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

📖 Docs preview deployed to https://multistore-docs-pr-152.development-seed.workers.dev

  • Date: 2026-09-25T16:05:01Z
  • Commit: d07fb55

Base automatically changed from refactor/split-proxy-tests to main September 25, 2026 06:00
alukach and others added 2 commits September 24, 2026 23:02
… infallibly

`with_user_agent` stored an unchecked `String` and every forwarded request
ran `self.user_agent.parse().unwrap()`, so a value with a control character
panicked on the first request instead of failing at startup. The setter now
takes `impl TryInto<HeaderValue>` and returns `Result`, and the gateway
stores a `HeaderValue`.

Constant content-type headers use `HeaderValue::from_static`, and the five
hand-built XML responses (ListBuckets, list, delete result, error response,
SlowDown in metering and the Workers rate limiter) go through the existing
`ProxyResult::xml` constructor.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`S3RequestSigner::sign_request` and `sign_s3_request` inserted runtime
strings (date, payload hash, session token, host, authorization) with
`parse().unwrap()`. A configured session token containing a control
character would panic mid-request. A small `header_value` helper maps an
invalid value to `ProxyError::Internal` without echoing the value, since it
may be a credential.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

🚀 Latest commit deployed to https://multistore-proxy-pr-152.development-seed.workers.dev

  • Date: 2026-09-25T06:11:36Z
  • Commit: 46d4a86

…#154)

* refactor: document every public item and remove production unwraps

Preparation for enabling `missing_docs` and `clippy::unwrap_used` at the
workspace level. Adds doc comments to the ~90 public fields, variants, and
functions that lacked them, and a `# Panics` section on `Router::route`.

Remaining `unwrap()` calls outside tests become either a recoverable path
(`JwksCache` mutex guards recover from poisoning; the Workers response
builder falls back to a 500) or an `expect` whose message states the
invariant (response builders fed already-valid status and headers).
`OidcBackendAuth::handle` takes the bucket config out of the context
instead of re-unwrapping it inside an `if let`.

Integration-test crates under `tests/` opt out of the unwrap lint at the
crate level; clippy's `allow-unwrap-in-tests` covers only `#[test]` fns and
`#[cfg(test)]` modules.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* chore: adopt workspace lints, declare MSRV, lint the Workers crates in CI

Adds `[workspace.lints]` with `missing_docs` and `clippy::unwrap_used` at
warn (CI already runs clippy with `-D warnings`), opted into by every crate
with `[lints] workspace = true`. `clippy.toml` exempts tests from the unwrap
lint.

Declares `rust-version = "1.89"`, the floor imposed by the dependency tree,
verified with `cargo +1.89 check`.

The Cloudflare crates are excluded from `default-members`, so the native
clippy job never linted them. The wasm CI job now runs clippy on them, with
a matching `make clippy-wasm` target and a CONTRIBUTING note.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
@alukach
alukach marked this pull request as ready for review September 25, 2026 16:05

This branch had an error being deployed

1 failed deployment
preview — 46af56b0 Deployed Sep 25, 2026 by alukach via Deploy & Test / Deploy #459
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant