From bd65cc561c0c7d70a7baf4e35296ce8c5b3da682 Mon Sep 17 00:00:00 2001 From: Aram Grigoryan <132480+aram356@users.noreply.github.com> Date: Sun, 17 May 2026 10:32:36 -0700 Subject: [PATCH 1/3] Cap /auction request body size before allocation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses review finding on #621 (P2-2): handle_auction parsed the request body with serde_json::from_slice(&req.take_body_bytes()) and no size limit, so an authenticated client could buffer arbitrary bytes into the Fastly Compute worker before any check. Reject oversized bodies with 413 — Content-Length precheck for well-behaved clients, post-read length check for clients that lie about or omit the header. Cap is 256 KiB — comfortable headroom for realistic Prebid-derived auctions. The check is inlined (rather than calling the content_length_exceeds_limit helper that batch_sync uses) because the HTTP-types migration in flight in http_util.rs is moving away from fastly::Request, and a shared helper would either fight that direction or force compat conversions at every call site. --- .../src/auction/endpoints.rs | 29 +++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/crates/trusted-server-core/src/auction/endpoints.rs b/crates/trusted-server-core/src/auction/endpoints.rs index 64832eef..d175be9f 100644 --- a/crates/trusted-server-core/src/auction/endpoints.rs +++ b/crates/trusted-server-core/src/auction/endpoints.rs @@ -1,6 +1,7 @@ //! HTTP endpoint handlers for auction requests. use error_stack::{Report, ResultExt}; +use fastly::http::StatusCode; use fastly::{Request, Response}; use serde_json::Value as JsonValue; @@ -27,6 +28,12 @@ const MAX_CLIENT_EID_SOURCES: usize = 64; const MAX_CLIENT_UIDS_PER_SOURCE: usize = 32; const MAX_CLIENT_EID_SOURCE_BYTES: usize = 255; +/// Maximum accepted JSON body size for `/auction`. Picked to comfortably fit +/// the largest realistic Prebid-derived auction request (hundreds of ad units +/// with EID arrays) while preventing an authenticated client from consuming +/// arbitrary WASM linear memory. +const MAX_AUCTION_BODY_SIZE: usize = 256 * 1024; + /// Handle auction request from /auction endpoint. /// /// This is the main entry point for running header bidding auctions. @@ -49,12 +56,24 @@ pub async fn handle_auction( services: &RuntimeServices, mut req: Request, ) -> Result> { - // Parse request body - let body: AdRequest = serde_json::from_slice(&req.take_body_bytes()).change_context( - TrustedServerError::Auction { + // Reject oversized bodies before any allocation. The `Content-Length` + // pre-check stops well-behaved clients early; the post-read check defends + // against clients that lie about (or omit) the header. + let content_length_exceeded = req + .get_header_str("content-length") + .and_then(|value| value.parse::().ok()) + .is_some_and(|length| length > MAX_AUCTION_BODY_SIZE); + if content_length_exceeded { + return Ok(Response::from_status(StatusCode::PAYLOAD_TOO_LARGE)); + } + let body_bytes = req.take_body_bytes(); + if body_bytes.len() > MAX_AUCTION_BODY_SIZE { + return Ok(Response::from_status(StatusCode::PAYLOAD_TOO_LARGE)); + } + let body: AdRequest = + serde_json::from_slice(&body_bytes).change_context(TrustedServerError::Auction { message: "Failed to parse auction request body".to_string(), - }, - )?; + })?; log::info!( "Auction request received for {} ad units", From 97331e4d33f83e124dd7c465c2cc6707497c89f1 Mon Sep 17 00:00:00 2001 From: Aram Grigoryan <132480+aram356@users.noreply.github.com> Date: Fri, 15 May 2026 15:26:23 -0700 Subject: [PATCH 2/3] Bundle IntegrationRegistry::handle_proxy inputs into a struct MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses review finding on #621 (P2-8): handle_proxy took 7 arguments including &self and was guarded by #[allow(clippy::too_many_arguments)]. CLAUDE.md disallows >7 arguments — use a struct. Introduce ProxyDispatchInput bundling method/path/settings/kv/ec_context/req under one lifetime, destructure inside the method, drop the allow, and update the four unit-test call sites plus the adapter-fastly dispatch site. --- .../trusted-server-adapter-fastly/src/main.rs | 11 ++- .../src/integrations/mod.rs | 3 +- .../src/integrations/registry.rs | 86 +++++++++++-------- 3 files changed, 62 insertions(+), 38 deletions(-) diff --git a/crates/trusted-server-adapter-fastly/src/main.rs b/crates/trusted-server-adapter-fastly/src/main.rs index e45ed776..7f97cada 100644 --- a/crates/trusted-server-adapter-fastly/src/main.rs +++ b/crates/trusted-server-adapter-fastly/src/main.rs @@ -24,7 +24,7 @@ use trusted_server_core::ec::registry::PartnerRegistry; use trusted_server_core::ec::EcContext; use trusted_server_core::error::TrustedServerError; use trusted_server_core::geo::GeoInfo; -use trusted_server_core::integrations::IntegrationRegistry; +use trusted_server_core::integrations::{IntegrationRegistry, ProxyDispatchInput}; use trusted_server_core::platform::RuntimeServices; use trusted_server_core::proxy::{ handle_first_party_click, handle_first_party_proxy, handle_first_party_proxy_rebuild, @@ -401,7 +401,14 @@ async fn route_request( } (m, path) if integration_registry.has_route(&m, path) => { let result = integration_registry - .handle_proxy(&m, path, settings, kv_graph.as_ref(), &mut ec_context, req) + .handle_proxy(ProxyDispatchInput { + method: &m, + path, + settings, + kv: kv_graph.as_ref(), + ec_context: &mut ec_context, + req, + }) .await .unwrap_or_else(|| { Err(Report::new(TrustedServerError::BadRequest { diff --git a/crates/trusted-server-core/src/integrations/mod.rs b/crates/trusted-server-core/src/integrations/mod.rs index 29657166..e9438b32 100644 --- a/crates/trusted-server-core/src/integrations/mod.rs +++ b/crates/trusted-server-core/src/integrations/mod.rs @@ -24,7 +24,8 @@ pub use registry::{ IntegrationAttributeRewriter, IntegrationDocumentState, IntegrationEndpoint, IntegrationHeadInjector, IntegrationHtmlContext, IntegrationHtmlPostProcessor, IntegrationMetadata, IntegrationProxy, IntegrationRegistration, IntegrationRegistrationBuilder, - IntegrationRegistry, IntegrationScriptContext, IntegrationScriptRewriter, ScriptRewriteAction, + IntegrationRegistry, IntegrationScriptContext, IntegrationScriptRewriter, ProxyDispatchInput, + ScriptRewriteAction, }; type IntegrationBuilder = diff --git a/crates/trusted-server-core/src/integrations/registry.rs b/crates/trusted-server-core/src/integrations/registry.rs index 0f9ddb33..389dd50d 100644 --- a/crates/trusted-server-core/src/integrations/registry.rs +++ b/crates/trusted-server-core/src/integrations/registry.rs @@ -539,6 +539,20 @@ impl IntegrationMetadata { } } +/// Inputs to [`IntegrationRegistry::handle_proxy`]. +/// +/// Bundled into a struct so the dispatch surface stays within the project's +/// 7-argument cap; `ec_context` and `req` participate in the borrow so the +/// whole thing shares one lifetime. +pub struct ProxyDispatchInput<'a> { + pub method: &'a Method, + pub path: &'a str, + pub settings: &'a Settings, + pub kv: Option<&'a KvIdentityGraph>, + pub ec_context: &'a mut EcContext, + pub req: Request, +} + /// In-memory registry of integrations discovered from settings. #[derive(Clone, Default)] pub struct IntegrationRegistry { @@ -656,17 +670,19 @@ impl IntegrationRegistry { /// /// This method removes any caller-supplied `x-ts-ec` before proxying. /// Response-side cookie mutation is centralized in EC finalize. - #[allow(clippy::too_many_arguments)] #[must_use] pub async fn handle_proxy( &self, - method: &Method, - path: &str, - settings: &Settings, - kv: Option<&KvIdentityGraph>, - ec_context: &mut EcContext, - mut req: Request, + input: ProxyDispatchInput<'_>, ) -> Option>> { + let ProxyDispatchInput { + method, + path, + settings, + kv, + ec_context, + mut req, + } = input; if let Some((proxy, _)) = self.find_route(method, path) { // Organic proxy handler: generate if needed (best effort). // Only generate for document navigations — subresource requests @@ -1295,14 +1311,14 @@ mod tests { EcContext::read_from_request(&settings, &req).expect("should read EC context"); // Call handle_proxy (uses futures executor in test environment) - let result = futures::executor::block_on(registry.handle_proxy( - &Method::GET, - "/integrations/test/ec", - &settings, - None, - &mut ec_context, + let result = futures::executor::block_on(registry.handle_proxy(ProxyDispatchInput { + method: &Method::GET, + path: "/integrations/test/ec", + settings: &settings, + kv: None, + ec_context: &mut ec_context, req, - )); + })); // Should have matched and returned a response assert!(result.is_some(), "should find route and handle request"); @@ -1335,14 +1351,14 @@ mod tests { let mut ec_context = EcContext::read_from_request(&settings, &req).expect("should read EC context"); - let result = futures::executor::block_on(registry.handle_proxy( - &Method::GET, - "/integrations/test/ec", - &settings, - None, - &mut ec_context, + let result = futures::executor::block_on(registry.handle_proxy(ProxyDispatchInput { + method: &Method::GET, + path: "/integrations/test/ec", + settings: &settings, + kv: None, + ec_context: &mut ec_context, req, - )) + })) .expect("should handle proxy request"); let response = result.expect("handler should succeed"); @@ -1372,14 +1388,14 @@ mod tests { let mut ec_context = EcContext::read_from_request(&settings, &req).expect("should read EC context"); - let result = futures::executor::block_on(registry.handle_proxy( - &Method::GET, - "/integrations/test/ec", - &settings, - None, - &mut ec_context, + let result = futures::executor::block_on(registry.handle_proxy(ProxyDispatchInput { + method: &Method::GET, + path: "/integrations/test/ec", + settings: &settings, + kv: None, + ec_context: &mut ec_context, req, - )) + })) .expect("should handle proxy request"); let response = result.expect("proxy handle should succeed"); @@ -1411,14 +1427,14 @@ mod tests { let mut ec_context = EcContext::read_from_request(&settings, &req).expect("should read EC context"); - let result = futures::executor::block_on(registry.handle_proxy( - &Method::POST, - "/integrations/test/ec", - &settings, - None, - &mut ec_context, + let result = futures::executor::block_on(registry.handle_proxy(ProxyDispatchInput { + method: &Method::POST, + path: "/integrations/test/ec", + settings: &settings, + kv: None, + ec_context: &mut ec_context, req, - )); + })); assert!(result.is_some(), "Should find POST route"); let response = result.unwrap(); From 9057abd332196629398ad378a4a36a581186a763 Mon Sep 17 00:00:00 2001 From: Aram Grigoryan <132480+aram356@users.noreply.github.com> Date: Fri, 15 May 2026 15:26:23 -0700 Subject: [PATCH 3/3] Clean up leftover synthetic-ID and removed-endpoint references Addresses review nitpicks on #621 (P2-10/11/12/13): - drop the dead VALID_SYNTHETIC_ID test constant + "synthetic ID" doc comment in test_support.rs (the EC system has no synthetic-ID concept any more) - rewrite the EcContext::consent_mut doc to stop citing the never- implemented /_ts/api/v1/sync endpoint - update the EcScenario doc comment to list only the EC endpoints that actually exist - extend the /identify response example in api-reference.md with cluster_size and note which fields are optional --- crates/integration-tests/tests/frameworks/scenarios.rs | 3 +-- crates/trusted-server-core/src/ec/mod.rs | 2 +- crates/trusted-server-core/src/test_support.rs | 4 ---- docs/guide/api-reference.md | 7 ++++++- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/crates/integration-tests/tests/frameworks/scenarios.rs b/crates/integration-tests/tests/frameworks/scenarios.rs index 8fc112fa..2e3ad3f7 100644 --- a/crates/integration-tests/tests/frameworks/scenarios.rs +++ b/crates/integration-tests/tests/frameworks/scenarios.rs @@ -435,8 +435,7 @@ impl CustomScenario { /// /// These run against the Viceroy runtime directly without a frontend /// framework container — they exercise EC-specific endpoints -/// (`/_ts/api/v1/sync`, `/_ts/api/v1/identify`, -/// `/_ts/api/v1/batch-sync`, `/_ts/admin/v1/partners/register`). +/// (`/_ts/api/v1/identify`, `/_ts/api/v1/batch-sync`). #[derive(Debug, Clone)] pub enum EcScenario { /// Seeded EC row → batch sync writes partner UID → identify (Bearer auth) diff --git a/crates/trusted-server-core/src/ec/mod.rs b/crates/trusted-server-core/src/ec/mod.rs index f20586c1..8fb9f5b7 100644 --- a/crates/trusted-server-core/src/ec/mod.rs +++ b/crates/trusted-server-core/src/ec/mod.rs @@ -334,7 +334,7 @@ impl EcContext { /// Returns a mutable reference to the consent context. /// - /// Used by `/_ts/api/v1/sync` to apply query-param fallback consent for the current + /// Allows handlers to apply query-param fallback consent for the current /// request only when pre-routing consent extraction produced an empty /// context. pub fn consent_mut(&mut self) -> &mut ConsentContext { diff --git a/crates/trusted-server-core/src/test_support.rs b/crates/trusted-server-core/src/test_support.rs index 39432755..25c918b2 100644 --- a/crates/trusted-server-core/src/test_support.rs +++ b/crates/trusted-server-core/src/test_support.rs @@ -2,10 +2,6 @@ pub mod tests { use crate::settings::Settings; - /// A well-formed synthetic ID for use in tests: 64 lowercase hex chars + `'.'` + 6 alphanumeric. - pub const VALID_SYNTHETIC_ID: &str = - "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2.Ab12z9"; - #[must_use] pub fn crate_test_settings_str() -> String { r#" diff --git a/docs/guide/api-reference.md b/docs/guide/api-reference.md index 208cb2c3..1af6d9aa 100644 --- a/docs/guide/api-reference.md +++ b/docs/guide/api-reference.md @@ -76,10 +76,15 @@ Returns EC identity plus the authenticated partner's UID and EID for the current "eid": { "source": "formally-vital-lion.edgecompute.app", "uids": [{ "id": "mock-user-123", "atype": 3 }] - } + }, + "cluster_size": 3 } ``` +`uid`, `eid`, and `cluster_size` are optional and omitted when unavailable +(e.g. no partner UID synced yet, KV read degraded, or cluster size not +re-evaluated within the recheck window). + --- ### POST /\_ts/api/v1/batch-sync