Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 91 additions & 1 deletion crates/trusted-server-adapter-fastly/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use error_stack::Report;
use fastly::http::Method;
use fastly::http::{header, Method};
use fastly::{Request, Response};

use trusted_server_core::auction::endpoints::handle_auction;
Expand Down Expand Up @@ -109,6 +109,33 @@ fn main() {
}
}

fn build_ja4_debug_response(req: &Request) -> Response {
let ja4 = req.get_tls_ja4().unwrap_or("unavailable");
let h2 = req.get_client_h2_fingerprint().unwrap_or("unavailable");
let cipher = req.get_tls_cipher_openssl_name().unwrap_or("unavailable");
let tls_version = req.get_tls_protocol().unwrap_or("unavailable");
let ua = req.get_header_str("user-agent").unwrap_or("none");
let ch_mobile = req.get_header_str("sec-ch-ua-mobile").unwrap_or("not sent");
let ch_platform = req
.get_header_str("sec-ch-ua-platform")
.unwrap_or("not sent");

let body = format!(
"ja4: {ja4}\n\
h2_fp: {h2}\n\
cipher: {cipher}\n\
tls_version: {tls_version}\n\
user-agent: {ua}\n\
ch-mobile: {ch_mobile}\n\
ch-platform: {ch_platform}\n"
);

Response::from_status(fastly::http::StatusCode::OK)
.with_header(header::CACHE_CONTROL, "no-store, private")
.with_content_type(fastly::mime::TEXT_PLAIN_UTF_8)
.with_body(body)
}

async fn route_request(
settings: &Settings,
orchestrator: &AuctionOrchestrator,
Expand Down Expand Up @@ -186,6 +213,9 @@ async fn route_request(
}
}

// Temporary JA4/TLS debug endpoint for browser fingerprint inspection.
(Method::GET, "/_ts/debug/ja4") => Ok(build_ja4_debug_response(&req)),
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gate the debug endpoint behind explicit configuration

The new GET /_ts/debug/ja4 route is currently reachable without authentication or an explicit debug gate. Because it reflects Fastly-observed JA4/H2/TLS details back to same-origin browser JavaScript, any script running on the publisher page could fetch this endpoint and exfiltrate fingerprinting values that browser JS normally cannot read directly.

Suggestion: Add an explicit trusted-server.toml configuration flag for this debug endpoint, disabled by default, and return 404 or another non-disclosing response when the flag is off. That keeps the endpoint available for intentional Fastly/browser investigation while avoiding a public-by-default fingerprint reflection API.


// tsjs endpoints
(Method::GET, "/first-party/proxy") => {
handle_first_party_proxy(settings, runtime_services, req).await
Expand Down Expand Up @@ -320,3 +350,63 @@ fn finalize_response(settings: &Settings, geo_info: Option<&GeoInfo>, response:
response.set_header(key, value);
}
}

#[cfg(test)]
mod tests {
use super::*;
use fastly::mime;

#[test]
fn ja4_debug_response_uses_plain_text_and_fallback_values() {
let req = Request::get("https://example.com/_ts/debug/ja4");

let mut response = build_ja4_debug_response(&req);

assert_eq!(
response.get_status(),
fastly::http::StatusCode::OK,
"should return 200 OK"
);
assert_eq!(
response.get_content_type(),
Some(mime::TEXT_PLAIN_UTF_8),
"should return plain text content"
);
assert_eq!(
response.get_header_str("cache-control"),
Some("no-store, private"),
"should disable caching for the debug response"
);

let body = response.take_body_str();

assert!(
body.contains("ja4: unavailable"),
"should include JA4 fallback"
);
assert!(
body.contains("h2_fp: unavailable"),
"should include H2 fingerprint fallback"
);
assert!(
body.contains("cipher: unavailable"),
"should include cipher fallback"
);
assert!(
body.contains("tls_version: unavailable"),
"should include TLS version fallback"
);
assert!(
body.contains("user-agent: none"),
"should include user-agent fallback"
);
assert!(
body.contains("ch-mobile: not sent"),
"should include sec-ch-ua-mobile fallback"
);
assert!(
body.contains("ch-platform: not sent"),
"should include sec-ch-ua-platform fallback"
);
}
}
68 changes: 67 additions & 1 deletion crates/trusted-server-adapter-fastly/src/route_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use std::sync::Arc;
use edgezero_core::key_value_store::NoopKvStore;
use error_stack::Report;
use fastly::http::StatusCode;
use fastly::Request;
use fastly::{mime, Request};
use trusted_server_core::auction::build_orchestrator;
use trusted_server_core::integrations::IntegrationRegistry;
use trusted_server_core::platform::{
Expand Down Expand Up @@ -249,3 +249,69 @@ fn configured_missing_consent_store_only_breaks_consent_routes() {
"should scope consent store failures to the consent-dependent routes"
);
}

#[test]
fn ja4_debug_route_returns_plain_text_fallback_response() {
let settings = create_test_settings();
let orchestrator = build_orchestrator(&settings).expect("should build auction orchestrator");
let integration_registry =
IntegrationRegistry::new(&settings).expect("should create integration registry");

let req = Request::get("https://test.com/_ts/debug/ja4");
let runtime_services = test_runtime_services(&req);
let mut response = futures::executor::block_on(route_request(
&settings,
&orchestrator,
&integration_registry,
&runtime_services,
req,
))
.expect("should route ja4 debug request");

assert_eq!(
response.get_status(),
StatusCode::OK,
"should return 200 OK for the ja4 debug route"
);
assert_eq!(
response.get_content_type(),
Some(mime::TEXT_PLAIN_UTF_8),
"should return plain text content for the ja4 debug route"
);
assert_eq!(
response.get_header_str("cache-control"),
Some("no-store, private"),
"should disable caching for the ja4 debug route"
);

let body = response.take_body_str();

assert!(
body.contains("ja4: unavailable"),
"should include the JA4 fallback when Fastly omits the fingerprint"
);
assert!(
body.contains("h2_fp: unavailable"),
"should include the H2 fingerprint fallback when Fastly omits it"
);
assert!(
body.contains("cipher: unavailable"),
"should include the cipher fallback when Fastly omits it"
);
assert!(
body.contains("tls_version: unavailable"),
"should include the TLS version fallback when Fastly omits it"
);
assert!(
body.contains("user-agent: none"),
"should include the user-agent fallback when the header is absent"
);
assert!(
body.contains("ch-mobile: not sent"),
"should include the mobile client hints fallback when the header is absent"
);
assert!(
body.contains("ch-platform: not sent"),
"should include the platform client hints fallback when the header is absent"
);
}
Loading