diff --git a/caddy/Caddyfile b/caddy/Caddyfile index 877d9d7..0c3e915 100644 --- a/caddy/Caddyfile +++ b/caddy/Caddyfile @@ -32,6 +32,15 @@ } :80 { + # Audit 2026-06-11 (H-1) — hard request-body ceiling at the proxy. + # Without this, Caddy forwards bodies up to its multi-GB default to + # picloud. 12 MB sits just above the orchestrator's 10 MiB user-route + # read so legitimate large invokes pass; admin / email handlers are + # bounded far tighter by their own Axum extractor limits. + request_body { + max_size 12MB + } + # Baseline headers applied to every response. header { X-Content-Type-Options "nosniff" diff --git a/caddy/Caddyfile.prod b/caddy/Caddyfile.prod index 28536e6..bf49c7f 100644 --- a/caddy/Caddyfile.prod +++ b/caddy/Caddyfile.prod @@ -19,6 +19,15 @@ {$PICLOUD_DOMAIN} { encode zstd gzip + # Audit 2026-06-11 (H-1) — hard request-body ceiling at the proxy. + # Without this, Caddy forwards bodies up to its multi-GB default to + # picloud. 12 MB sits just above the orchestrator's 10 MiB user-route + # read so legitimate large invokes pass; admin / email handlers are + # bounded far tighter by their own Axum extractor limits. + request_body { + max_size 12MB + } + # Baseline headers on every response. HSTS is unconditional in prod. header { Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" diff --git a/crates/manager-core/src/email_inbound_api.rs b/crates/manager-core/src/email_inbound_api.rs index faa7b68..f8732cc 100644 --- a/crates/manager-core/src/email_inbound_api.rs +++ b/crates/manager-core/src/email_inbound_api.rs @@ -169,12 +169,20 @@ pub struct EmailInboundState { pub bad_sig_limiter: Arc, } +/// Per-request body cap for the inbound webhook. Inbound email events +/// are small JSON envelopes (from/to/subject/body); 1 MiB is generous. +/// Audit 2026-06-11 (H-1) — this public, unauthenticated endpoint +/// otherwise rode Axum's 2 MB extractor default; pin it explicitly and +/// tighter so a flood can't force large allocations + JSON parses. +const INBOUND_BODY_LIMIT_BYTES: usize = 1024 * 1024; + pub fn email_inbound_router(state: EmailInboundState) -> Router { Router::new() .route( "/email-inbound/{app_id}/{trigger_id}", post(receive_inbound_email), ) + .layer(axum::extract::DefaultBodyLimit::max(INBOUND_BODY_LIMIT_BYTES)) .with_state(state) }