Skip to content
25 changes: 25 additions & 0 deletions crates/common/src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,28 @@ pub const HEADER_ACCEPT: HeaderName = HeaderName::from_static("accept");
pub const HEADER_ACCEPT_LANGUAGE: HeaderName = HeaderName::from_static("accept-language");
pub const HEADER_ACCEPT_ENCODING: HeaderName = HeaderName::from_static("accept-encoding");
pub const HEADER_REFERER: HeaderName = HeaderName::from_static("referer");

/// TS-internal header names that must NOT be forwarded to downstream third-party services.
///
/// These headers are used internally by Trusted Server for identity, geo-enrichment,
/// debugging, and compression hints. Leaking them to external origins could expose
/// user tracking data and internal implementation details.
///
/// Uses `&str` slices because `HeaderName` has interior mutability and cannot appear
/// in `const` context.
pub const INTERNAL_HEADERS: &[&str] = &[
"x-synthetic-id",
"x-pub-user-id",
"x-subject-id",
"x-consent-advertising",
"x-geo-city",
"x-geo-continent",
"x-geo-coordinates",
"x-geo-country",
"x-geo-info-available",
"x-geo-metro-code",
"x-geo-region",
"x-request-id",
"x-compress-hint",
"x-debug-fastly-pop",
];
35 changes: 35 additions & 0 deletions crates/common/src/cookies.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,21 @@ pub fn create_synthetic_cookie(settings: &Settings, synthetic_id: &str) -> Strin
)
}

/// Sets the synthetic ID cookie on the given response.
///
/// This helper abstracts the logic of creating the cookie string and appending
/// the Set-Cookie header to the response.
pub fn set_synthetic_cookie(
settings: &Settings,
response: &mut fastly::Response,
synthetic_id: &str,
) {
response.append_header(
header::SET_COOKIE,
create_synthetic_cookie(settings, synthetic_id),
);
}

#[cfg(test)]
mod tests {
use crate::test_support::tests::create_test_settings;
Expand Down Expand Up @@ -164,4 +179,24 @@ mod tests {
)
);
}

#[test]
fn test_set_synthetic_cookie() {
let settings = create_test_settings();
let mut response = fastly::Response::new();
set_synthetic_cookie(&settings, &mut response, "test-id-123");

let cookie_header = response
.get_header(header::SET_COOKIE)
.expect("Set-Cookie header should be present");
let cookie_str = cookie_header
.to_str()
.expect("header should be valid UTF-8");

let expected = create_synthetic_cookie(&settings, "test-id-123");
assert_eq!(
cookie_str, expected,
"Set-Cookie header should match create_synthetic_cookie output"
);
}
}
52 changes: 52 additions & 0 deletions crates/common/src/http_util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,28 @@ use fastly::http::{header, StatusCode};
use fastly::{Request, Response};
use sha2::{Digest, Sha256};

use crate::constants::INTERNAL_HEADERS;
use crate::settings::Settings;

/// Copy `X-*` custom headers from one request to another, skipping TS-internal headers.
///
/// This filters out all headers listed in [`INTERNAL_HEADERS`] to prevent leaking
/// internal identity, geo-enrichment, and debugging data to downstream third-party
/// services. Integrations that forward custom headers should use this utility
/// instead of manually iterating over header names.
pub fn copy_custom_headers(from: &Request, to: &mut Request) {
for header_name in from.get_header_names() {
let name_str = header_name.as_str();
if (name_str.starts_with("x-") || name_str.starts_with("X-"))
&& !INTERNAL_HEADERS.contains(&name_str)
{
if let Some(value) = from.get_header(header_name) {
to.set_header(header_name, value);
}
}
}
}

/// Extracted request information for host rewriting.
///
/// This struct captures the effective host and scheme from an incoming request,
Expand Down Expand Up @@ -440,4 +460,36 @@ mod tests {
"Scheme should use X-Forwarded-Proto in chained proxy scenarios"
);
}

#[test]
fn test_copy_custom_headers_filters_internal() {
let mut req = Request::new(fastly::http::Method::GET, "https://example.com");
req.set_header("x-custom-1", "value1");
// HeaderName is case-insensitive and always lowercase, but set_header accepts strings
req.set_header("X-Custom-2", "value2");
req.set_header("x-synthetic-id", "should not copy");
req.set_header("x-geo-country", "US");

let mut target = Request::new(fastly::http::Method::GET, "https://target.com");
copy_custom_headers(&req, &mut target);

assert_eq!(
target.get_header("x-custom-1").unwrap().to_str().unwrap(),
"value1",
"Should copy arbitrary x-header"
);
assert_eq!(
target.get_header("x-custom-2").unwrap().to_str().unwrap(),
"value2",
"Should copy arbitary X-header (case insensitive)"
);
assert!(
target.get_header("x-synthetic-id").is_none(),
"Should filter x-synthetic-id"
);
assert!(
target.get_header("x-geo-country").is_none(),
"Should filter x-geo-country"
);
}
}
12 changes: 3 additions & 9 deletions crates/common/src/integrations/lockr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ use validator::Validate;

use crate::backend::ensure_backend_from_url;
use crate::error::TrustedServerError;
use crate::http_util::copy_custom_headers;
use crate::integrations::{
AttributeRewriteAction, IntegrationAttributeContext, IntegrationAttributeRewriter,
IntegrationEndpoint, IntegrationProxy, IntegrationRegistration,
Expand Down Expand Up @@ -290,15 +291,8 @@ impl LockrIntegration {
to.set_header(header::ORIGIN, origin);
}

// Copy any X-* custom headers
for header_name in from.get_header_names() {
let name_str = header_name.as_str();
if name_str.starts_with("x-") || name_str.starts_with("X-") {
if let Some(value) = from.get_header(header_name) {
to.set_header(header_name, value);
}
}
}
// Copy any X-* custom headers, skipping TS-internal headers
copy_custom_headers(from, to);
}
}

Expand Down
12 changes: 3 additions & 9 deletions crates/common/src/integrations/permutive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use validator::Validate;

use crate::backend::ensure_backend_from_url;
use crate::error::TrustedServerError;
use crate::http_util::copy_custom_headers;
use crate::integrations::{
AttributeRewriteAction, IntegrationAttributeContext, IntegrationAttributeRewriter,
IntegrationEndpoint, IntegrationProxy, IntegrationRegistration,
Expand Down Expand Up @@ -495,15 +496,8 @@ impl PermutiveIntegration {
}
}

// Copy any X-* custom headers
for header_name in from.get_header_names() {
let name_str = header_name.as_str();
if name_str.starts_with("x-") || name_str.starts_with("X-") {
if let Some(value) = from.get_header(header_name) {
to.set_header(header_name, value);
}
}
}
// Copy any X-* custom headers, skipping TS-internal headers
copy_custom_headers(from, to);
}
}

Expand Down
Loading