Sanitize cookie value before Set-Cookie interpolation#509
Sanitize cookie value before Set-Cookie interpolation#509
Conversation
Validate synthetic_id against RFC 6265 cookie-octet rules in set_synthetic_cookie() before header interpolation. Values containing semicolons, whitespace, control characters or non-ASCII bytes are rejected with a warning log and no cookie is set, preventing header-injection attacks.
ChristianPavilonis
left a comment
There was a problem hiding this comment.
PR Review — Staff-Engineer Pass
Findings: 0 blockers, 0 high, 3 medium, 2 low
This is a clean, well-executed security fix. The core RFC 6265 validation is correct and placed at the right architectural boundary. The findings below are quality improvements rather than correctness issues.
🤔 P2 — Empty string passes validation
crates/common/src/cookies.rs line 71–82
is_safe_cookie_value("") returns true because Iterator::all on an empty iterator is vacuously true. This would produce a Set-Cookie header like synthetic_id=; Domain=... — technically valid per RFC 6265 but semantically meaningless. Since this is a security boundary function, the contract should be explicit:
fn is_safe_cookie_value(value: &str) -> bool {
!value.is_empty()
&& value
.bytes()
.all(|b| matches!(b, 0x21 | 0x23..=0x2B | 0x2D..=0x3A | 0x3C..=0x5B | 0x5D..=0x7E))
}Add a corresponding test:
#[test]
fn test_is_safe_cookie_value_rejects_empty_string() {
assert!(!is_safe_cookie_value(""), "should reject empty string");
}🤔 P2 — Missing #[must_use] on is_safe_cookie_value
crates/common/src/cookies.rs line 71
Security-critical validation function whose return value must never be silently discarded. The original create_synthetic_cookie had #[must_use]. Adding it here protects against future callers accidentally ignoring the result:
#[must_use]
fn is_safe_cookie_value(value: &str) -> bool {🤔 P2 — Missing doc comment on create_synthetic_cookie
crates/common/src/cookies.rs line 84
The original function had a doc comment which was removed when visibility was narrowed to private. Even private functions that format security-sensitive headers benefit from a brief doc comment:
/// Formats the `Set-Cookie` header value for the synthetic ID cookie.
fn create_synthetic_cookie(settings: &Settings, synthetic_id: &str) -> String {⛏ P3 — Log message could include value length for diagnostics
crates/common/src/cookies.rs line 105–107
The warning doesn't include any context about the offending value. Logging the byte length helps operators diagnose issues without exposing the raw value:
log::warn!(
"Rejecting synthetic_id for Set-Cookie: value of {} bytes contains characters illegal in a cookie value",
synthetic_id.len()
);👍 What's good
- RFC 6265
cookie-octetbyte ranges are correct — verified against the spec - Validation at the right boundary (
set_synthetic_cookie) — single point of enforcement - Visibility narrowing of
create_synthetic_cookieto private — no external callers exist - Explanatory comments at both call sites prevent future confusion about intentional skip
- Comprehensive test coverage: semicolon injection, CRLF, whitespace, non-ASCII, each illegal character class
- Minimal, focused change: 3 files, ~120 lines, no new dependencies, no unrelated refactoring
Summary
synthetic_idcontaining RFC 6265-illegal characters (e.g. a semicolon) could inject spurious cookie attributes into theSet-Cookieheader, enabling header-injection attacks.is_safe_cookie_value()which validates every byte of the ID against the RFC 6265cookie-octetallowlist before interpolation.X-Synthetic-IDresponse header is still emitted unchanged.Changes
crates/common/src/cookies.rsis_safe_cookie_value()RFC 6265 validator; gateset_synthetic_cookie()on it; narrowcreate_synthetic_cookie()visibility to private; expand tests with injection, CRLF, space, non-ASCII, and valid-character casescrates/common/src/publisher.rscrates/common/src/integrations/registry.rsCloses
Closes #413
Test plan
cargo test --workspacecargo clippy --all-targets --all-features -- -D warningscargo fmt --all -- --checkcd crates/js/lib && npm run formatcd docs && npm run formatChecklist
unwrap()in production code — useexpect("should ...")logmacros (notprintln!)