Skip to content

Commit ca92c2e

Browse files
committed
Validate synthetic ID format on inbound header and cookie values
Inbound synthetic IDs from the x-synthetic-id header and synthetic_id cookie were accepted without validation. An attacker could inject arbitrary strings — including very long values, special characters, or newlines — which were then set as response headers, cookies, and forwarded to third-party APIs. Adds a private is_valid_synthetic_id() validator enforcing the canonical format (64 lowercase hex chars + '.' + 6 alphanumeric chars). The length check is O(1) and runs first to bound all downstream work. Invalid values are silently discarded and a fresh ID is generated in their place; the raw value is never written to logs. Also adds a debug_assert! in generate_synthetic_id() to catch any future regression in the generator, moves VALID_SYNTHETIC_ID to test_support so it is shared across all test modules, and demotes synthetic ID values from INFO to DEBUG in log output to avoid recording pseudonymous identifiers in production log pipelines. Closes #412
1 parent e295f3a commit ca92c2e

6 files changed

Lines changed: 215 additions & 74 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Security
11+
12+
- Validate synthetic ID format on inbound values from the `x-synthetic-id` header and `synthetic_id` cookie; values that do not match the expected format (`64-hex-hmac.6-alphanumeric-suffix`) are discarded and a fresh ID is generated rather than forwarded to response headers, cookies, or third-party APIs
13+
1014
### Added
1115

1216
- Implemented basic authentication for configurable endpoint paths (#73)

crates/common/src/integrations/registry.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1310,8 +1310,15 @@ mod tests {
13101310
let registry = IntegrationRegistry::from_routes(routes);
13111311

13121312
let mut req = Request::get("https://test.example.com/integrations/test/synthetic");
1313-
// Pre-existing cookie
1314-
req.set_header(header::COOKIE, "synthetic_id=existing_id_12345");
1313+
// Pre-existing cookie with a valid-format synthetic ID
1314+
req.set_header(
1315+
header::COOKIE,
1316+
format!(
1317+
"{}={}",
1318+
crate::constants::COOKIE_SYNTHETIC_ID,
1319+
crate::test_support::tests::VALID_SYNTHETIC_ID
1320+
),
1321+
);
13151322

13161323
let result = futures::executor::block_on(registry.handle_proxy(
13171324
&Method::GET,

crates/common/src/proxy.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1291,7 +1291,8 @@ mod tests {
12911291
sig
12921292
),
12931293
);
1294-
req.set_header(crate::constants::HEADER_X_SYNTHETIC_ID, "synthetic-123");
1294+
let valid_synthetic_id = crate::test_support::tests::VALID_SYNTHETIC_ID;
1295+
req.set_header(crate::constants::HEADER_X_SYNTHETIC_ID, valid_synthetic_id);
12951296

12961297
let resp = handle_first_party_click(&settings, req)
12971298
.await
@@ -1309,7 +1310,7 @@ mod tests {
13091310
assert_eq!(pairs.remove("foo").as_deref(), Some("1"));
13101311
assert_eq!(
13111312
pairs.remove("synthetic_id").as_deref(),
1312-
Some("synthetic-123")
1313+
Some(valid_synthetic_id)
13131314
);
13141315
assert!(pairs.is_empty());
13151316
}

0 commit comments

Comments
 (0)