Conversation
The synthetic_id cookie was missing the HttpOnly attribute, allowing any XSS on a publisher page to exfiltrate the tracking identifier via document.cookie. HttpOnly is safe to set because integrations receive the synthetic ID via the x-synthetic-id response header and no client-side JS reads it from the cookie directly. Also documents the rationale for each security attribute (Secure, HttpOnly, SameSite=Lax, Max-Age) in the doc comment, and adds debug_assert guards against cookie metacharacter injection in both the synthetic_id value and cookie_domain. Closes #411
ChristianPavilonis
left a comment
There was a problem hiding this comment.
Review Summary
Clean, well-scoped security hardening. The HttpOnly addition is correct, the doc comment is excellent, and the change is minimal. One concern about the debug_assert! approach and a few smaller items below.
0 Blockers · 1 High · 3 Medium
| #[must_use] | ||
| pub fn create_synthetic_cookie(settings: &Settings, synthetic_id: &str) -> String { | ||
| debug_assert!( | ||
| !synthetic_id.contains([';', '=', '\n', '\r']), |
There was a problem hiding this comment.
P1 — debug_assert! provides no protection in release builds
debug_assert! is compiled out in --release, which is exactly how this runs on Fastly Compute. The synthetic_id value comes from untrusted input (x-synthetic-id request header or the inbound cookie), so a crafted value like evil; Domain=.attacker.com would pass straight through in production.
Consider runtime sanitization here, e.g.:
let safe_id: String = synthetic_id
.chars()
.filter(|c| !matches!(c, ';' | '=' | '\n' | '\r'))
.collect();Or validate upstream in get_synthetic_id() with an allowlist pattern. The cookie_domain assert below is lower risk (config-sourced), but even there a regular assert! would catch config mistakes in production.
| /// - `SameSite=Lax`: sent on same-site requests and top-level cross-site navigations. | ||
| /// `Strict` is intentionally avoided — it would suppress the cookie on the first | ||
| /// request when a user arrives from an external page, breaking first-visit attribution. | ||
| /// - `Max-Age`: 1 year retention. |
There was a problem hiding this comment.
P2 — Missing # Panics doc section
Per project conventions, functions that can panic should document it. The debug_assert! macros below will panic in debug/test builds. Consider adding:
/// # Panics
///
/// Panics in debug builds if `synthetic_id` or `cookie_domain` contains
/// cookie metacharacters (`;`, `=`, `\n`, `\r`).
| "synthetic_id value should not contain cookie metacharacters" | ||
| ); | ||
| debug_assert!( | ||
| !settings.publisher.cookie_domain.contains([';', '\n', '\r']), |
There was a problem hiding this comment.
P2 — Asymmetric metacharacter lists deserve a comment
The synthetic_id check includes '=' but this one omits it. This is actually correct — = separates name from value so it only has special meaning in the value position, not in a domain attribute — but the asymmetry is subtle. A brief inline comment would help future readers:
// `=` is excluded: it only has special meaning in the name=value pair,
// not within an attribute like Domain.| debug_assert!( | ||
| !synthetic_id.contains([';', '=', '\n', '\r']), | ||
| "synthetic_id value should not contain cookie metacharacters" | ||
| ); |
There was a problem hiding this comment.
P2 — No test coverage for the assertion guards
Consider adding #[should_panic] tests to document the invariant and verify the assertions fire:
#[test]
#[should_panic(expected = "should not contain cookie metacharacters")]
fn test_create_synthetic_cookie_rejects_semicolon_in_id() {
let settings = create_test_settings();
create_synthetic_cookie(&settings, "evil;injected");
}
Summary
synthetic_idcookie was missingHttpOnly, allowing XSS on a publisher page to exfiltrate the tracking identifier viadocument.cookie.HttpOnlyis safe to add because no client-side JS reads this cookie — integrations receive the synthetic ID via thex-synthetic-idresponse header instead.debug_assert!guards against cookie metacharacter injection in both thesynthetic_idvalue andcookie_domain, and documents the rationale for each security attribute in the function's doc comment.Changes
crates/common/src/cookies.rsHttpOnlyto cookie format string; adddebug_assert!metacharacter guards; update doc comment to enumerate all security attributes with rationaleCloses
Closes #411
Test plan
cargo test --workspacecargo clippy --all-targets --all-features -- -D warningscargo fmt --all -- --checkcd crates/js/lib && npx vitest run— pre-existing ESM/CJS failure onmain, unrelated to this changecd crates/js/lib && npm run formatcd docs && npm run formatcargo build --bin trusted-server-fastly --release --target wasm32-wasip1fastly compute serveChecklist
unwrap()in production code — useexpect("should ...")tracingmacros (notprintln!)