Skip to content

Commit 08634ca

Browse files
authored
Set x-synthetic-id header on integration routes for response from Trusted Server (#255)
* Set x-synthetic-id header on integration routes Integration proxy responses were missing the x-synthetic-id header because handle_proxy dispatched to integrations without adding it. This caused identity tracking to break for first-party re-hosted integrations like Permutive's secure-signal endpoint. Centralizing the header logic in handle_proxy ensures all current and future integrations automatically include the synthetic ID, rather than requiring each integration to implement it manually. Fixes #205
1 parent 1baf1e0 commit 08634ca

9 files changed

Lines changed: 343 additions & 51 deletions

File tree

crates/common/src/constants.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,29 @@ pub const HEADER_ACCEPT: HeaderName = HeaderName::from_static("accept");
3333
pub const HEADER_ACCEPT_LANGUAGE: HeaderName = HeaderName::from_static("accept-language");
3434
pub const HEADER_ACCEPT_ENCODING: HeaderName = HeaderName::from_static("accept-encoding");
3535
pub const HEADER_REFERER: HeaderName = HeaderName::from_static("referer");
36+
37+
/// TS-internal header names that must NOT be forwarded to downstream third-party services.
38+
///
39+
/// These headers are used internally by Trusted Server for identity, geo-enrichment,
40+
/// debugging, and compression hints. Leaking them to external origins could expose
41+
/// user tracking data and internal implementation details.
42+
///
43+
/// Uses `&str` slices because `HeaderName` has interior mutability and cannot appear
44+
/// in `const` context.
45+
pub const INTERNAL_HEADERS: &[&str] = &[
46+
"x-synthetic-id",
47+
"x-pub-user-id",
48+
"x-subject-id",
49+
"x-consent-advertising",
50+
"x-forwarded-for",
51+
"x-geo-city",
52+
"x-geo-continent",
53+
"x-geo-coordinates",
54+
"x-geo-country",
55+
"x-geo-info-available",
56+
"x-geo-metro-code",
57+
"x-geo-region",
58+
"x-request-id",
59+
"x-compress-hint",
60+
"x-debug-fastly-pop",
61+
];

crates/common/src/cookies.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,21 @@ pub fn create_synthetic_cookie(settings: &Settings, synthetic_id: &str) -> Strin
7171
)
7272
}
7373

74+
/// Sets the synthetic ID cookie on the given response.
75+
///
76+
/// This helper abstracts the logic of creating the cookie string and appending
77+
/// the Set-Cookie header to the response.
78+
pub fn set_synthetic_cookie(
79+
settings: &Settings,
80+
response: &mut fastly::Response,
81+
synthetic_id: &str,
82+
) {
83+
response.append_header(
84+
header::SET_COOKIE,
85+
create_synthetic_cookie(settings, synthetic_id),
86+
);
87+
}
88+
7489
#[cfg(test)]
7590
mod tests {
7691
use crate::test_support::tests::create_test_settings;
@@ -164,4 +179,24 @@ mod tests {
164179
)
165180
);
166181
}
182+
183+
#[test]
184+
fn test_set_synthetic_cookie() {
185+
let settings = create_test_settings();
186+
let mut response = fastly::Response::new();
187+
set_synthetic_cookie(&settings, &mut response, "test-id-123");
188+
189+
let cookie_header = response
190+
.get_header(header::SET_COOKIE)
191+
.expect("Set-Cookie header should be present");
192+
let cookie_str = cookie_header
193+
.to_str()
194+
.expect("header should be valid UTF-8");
195+
196+
let expected = create_synthetic_cookie(&settings, "test-id-123");
197+
assert_eq!(
198+
cookie_str, expected,
199+
"Set-Cookie header should match create_synthetic_cookie output"
200+
);
201+
}
167202
}

crates/common/src/http_util.rs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,28 @@ use fastly::http::{header, StatusCode};
44
use fastly::{Request, Response};
55
use sha2::{Digest, Sha256};
66

7+
use crate::constants::INTERNAL_HEADERS;
78
use crate::settings::Settings;
89

10+
/// Copy `X-*` custom headers from one request to another, skipping TS-internal headers.
11+
///
12+
/// This filters out all headers listed in [`INTERNAL_HEADERS`] to prevent leaking
13+
/// internal identity, geo-enrichment, and debugging data to downstream third-party
14+
/// services. Integrations that forward custom headers should use this utility
15+
/// instead of manually iterating over header names.
16+
pub fn copy_custom_headers(from: &Request, to: &mut Request) {
17+
for header_name in from.get_header_names() {
18+
let name_str = header_name.as_str();
19+
if (name_str.starts_with("x-") || name_str.starts_with("X-"))
20+
&& !INTERNAL_HEADERS.contains(&name_str)
21+
{
22+
if let Some(value) = from.get_header(header_name) {
23+
to.set_header(header_name, value);
24+
}
25+
}
26+
}
27+
}
28+
929
/// Extracted request information for host rewriting.
1030
///
1131
/// This struct captures the effective host and scheme from an incoming request,
@@ -447,4 +467,36 @@ mod tests {
447467
"Scheme should use X-Forwarded-Proto in chained proxy scenarios"
448468
);
449469
}
470+
471+
#[test]
472+
fn test_copy_custom_headers_filters_internal() {
473+
let mut req = Request::new(fastly::http::Method::GET, "https://example.com");
474+
req.set_header("x-custom-1", "value1");
475+
// HeaderName is case-insensitive and always lowercase, but set_header accepts strings
476+
req.set_header("X-Custom-2", "value2");
477+
req.set_header("x-synthetic-id", "should not copy");
478+
req.set_header("x-geo-country", "US");
479+
480+
let mut target = Request::new(fastly::http::Method::GET, "https://target.com");
481+
copy_custom_headers(&req, &mut target);
482+
483+
assert_eq!(
484+
target.get_header("x-custom-1").unwrap().to_str().unwrap(),
485+
"value1",
486+
"Should copy arbitrary x-header"
487+
);
488+
assert_eq!(
489+
target.get_header("x-custom-2").unwrap().to_str().unwrap(),
490+
"value2",
491+
"Should copy arbitrary X-header (case insensitive)"
492+
);
493+
assert!(
494+
target.get_header("x-synthetic-id").is_none(),
495+
"Should filter x-synthetic-id"
496+
);
497+
assert!(
498+
target.get_header("x-geo-country").is_none(),
499+
"Should filter x-geo-country"
500+
);
501+
}
450502
}

crates/common/src/integrations/lockr.rs

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ use validator::Validate;
2525

2626
use crate::backend::BackendConfig;
2727
use crate::error::TrustedServerError;
28+
use crate::http_util::copy_custom_headers;
2829
use crate::integrations::{
2930
AttributeRewriteAction, IntegrationAttributeContext, IntegrationAttributeRewriter,
3031
IntegrationEndpoint, IntegrationProxy, IntegrationRegistration,
@@ -287,15 +288,8 @@ impl LockrIntegration {
287288
to.set_header(header::ORIGIN, origin);
288289
}
289290

290-
// Copy any X-* custom headers
291-
for header_name in from.get_header_names() {
292-
let name_str = header_name.as_str();
293-
if name_str.starts_with("x-") || name_str.starts_with("X-") {
294-
if let Some(value) = from.get_header(header_name) {
295-
to.set_header(header_name, value);
296-
}
297-
}
298-
}
291+
// Copy any X-* custom headers, skipping TS-internal headers
292+
copy_custom_headers(from, to);
299293
}
300294
}
301295

crates/common/src/integrations/permutive.rs

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use validator::Validate;
1414

1515
use crate::backend::BackendConfig;
1616
use crate::error::TrustedServerError;
17+
use crate::http_util::copy_custom_headers;
1718
use crate::integrations::{
1819
AttributeRewriteAction, IntegrationAttributeContext, IntegrationAttributeRewriter,
1920
IntegrationEndpoint, IntegrationProxy, IntegrationRegistration,
@@ -492,15 +493,8 @@ impl PermutiveIntegration {
492493
}
493494
}
494495

495-
// Copy any X-* custom headers
496-
for header_name in from.get_header_names() {
497-
let name_str = header_name.as_str();
498-
if name_str.starts_with("x-") || name_str.starts_with("X-") {
499-
if let Some(value) = from.get_header(header_name) {
500-
to.set_header(header_name, value);
501-
}
502-
}
503-
}
496+
// Copy any X-* custom headers, skipping TS-internal headers
497+
copy_custom_headers(from, to);
504498
}
505499
}
506500

0 commit comments

Comments
 (0)