VYPR
High severityNVD Advisory· Published Mar 6, 2024· Updated Aug 5, 2024

Apollo Router's Compressed Payloads do not respect HTTP Payload Limits

CVE-2024-28101

Description

The Apollo Router is a graph router written in Rust to run a federated supergraph that uses Apollo Federation. Versions 0.9.5 until 1.40.2 are subject to a Denial-of-Service (DoS) type vulnerability. When receiving compressed HTTP payloads, affected versions of the Router evaluate the limits.http_max_request_bytes configuration option after the entirety of the compressed payload is decompressed. If affected versions of the Router receive highly compressed payloads, this could result in significant memory consumption while the compressed payload is expanded. Router version 1.40.2 has a fix for the vulnerability. Those who are unable to upgrade may be able to implement mitigations at proxies or load balancers positioned in front of their Router fleet (e.g. Nginx, HAProxy, or cloud-native WAF services) by creating limits on HTTP body upload size.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

A DoS vulnerability in Apollo Router allows attackers to exhaust memory by sending highly compressed payloads that bypass size limits until decompression.

Vulnerability

Description

The Apollo Router, a configurable graph router for federated supergraphs written in Rust, is subject to a denial-of-service (DoS) vulnerability in versions 0.9.5 through 1.40.1. The root cause lies in the order of operations when processing compressed HTTP payloads: the router evaluates the limits.http_max_request_bytes configuration option *after* the entire compressed payload has been decompressed [3]. This allows an attacker to send a small, highly compressed request that expands to consume significant memory during decompression, bypassing the configured byte limit [3].

Exploitation

Exploitation requires the ability to send HTTP requests to an affected Apollo Router instance. The attack surface is remote and does not require authentication. An attacker crafts compressed HTTP requests (e.g., using gzip or other compression schemes) with a high compression ratio, causing the router to allocate and expand the payload in memory before checking the size limit [3]. No special privileges or network position is required beyond network access to the router's endpoint.

Impact

Successful exploitation can lead to memory exhaustion on the router process, resulting in a denial-of-service condition. This can degrade or completely block legitimate graph query processing, affecting all federated services served by that router instance [3]. The vulnerability does not require authentication and can be triggered by a single or small number of carefully crafted requests.

Mitigation

Version 1.40.2 of the Apollo Router includes a fix that ensures size limits are applied to compressed payloads before or during decompression [1][2]. Users unable to upgrade can implement mitigations at proxies or load balancers positioned in front of the router fleet—such as Nginx, HAProxy, or cloud-native WAF services—by enforcing limits on HTTP body upload size [3]. No other workarounds have been released.

AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
apollo-routercrates.io
>= 0.9.5, < 1.40.21.40.2

Affected products

2

Patches

1
9e9527c73c8f

streaming body decompression

https://github.com/apollographql/routerGeoffroy CouprieMar 6, 2024via ghsa
6 files changed · +61 108
  • apollo-router/src/axum_factory/axum_http_server_factory.rs+34 13 modified
    @@ -6,6 +6,7 @@ use std::sync::atomic::Ordering;
     use std::sync::Arc;
     use std::time::Instant;
     
    +use axum::error_handling::HandleErrorLayer;
     use axum::extract::Extension;
     use axum::extract::State;
     use axum::http::StatusCode;
    @@ -32,14 +33,15 @@ use tokio::sync::mpsc;
     use tokio_rustls::TlsAcceptor;
     use tower::service_fn;
     use tower::BoxError;
    +use tower::ServiceBuilder;
     use tower::ServiceExt;
    +use tower_http::decompression::DecompressionBody;
     use tower_http::trace::TraceLayer;
     
     use super::listeners::ensure_endpoints_consistency;
     use super::listeners::ensure_listenaddrs_consistency;
     use super::listeners::extra_endpoints;
     use super::listeners::ListenersAndRouters;
    -use super::utils::decompress_request_body;
     use super::utils::PropagatingMakeSpan;
     use super::ListenAddrAndRouter;
     use super::ENDPOINT_CALLBACK;
    @@ -57,6 +59,7 @@ use crate::plugins::traffic_shaping::RateLimited;
     use crate::router::ApolloRouterError;
     use crate::router_factory::Endpoint;
     use crate::router_factory::RouterFactory;
    +use crate::services::http::service::BodyStream;
     use crate::services::router;
     use crate::uplink::license_enforcement::LicenseState;
     use crate::uplink::license_enforcement::APOLLO_ROUTER_LICENSE_EXPIRED;
    @@ -173,11 +176,9 @@ where
                         tracing::trace!(?health, request = ?req.router_request, "health check");
                         async move {
                             Ok(router::Response {
    -                            response: http::Response::builder()
    -                                .status(status_code)
    -                                .body::<hyper::Body>(
    -                                    serde_json::to_vec(&health).map_err(BoxError::from)?.into(),
    -                                )?,
    +                            response: http::Response::builder().status(status_code).body::<Body>(
    +                                serde_json::to_vec(&health).map_err(BoxError::from)?.into(),
    +                            )?,
                                 context: req.context,
                             })
                         }
    @@ -422,6 +423,10 @@ pub(crate) fn span_mode(configuration: &Configuration) -> SpanMode {
             .unwrap_or_default()
     }
     
    +async fn decompression_error(_error: BoxError) -> axum::response::Response {
    +    (StatusCode::BAD_REQUEST, "cannot decompress request body").into_response()
    +}
    +
     fn main_endpoint<RF>(
         service_factory: RF,
         configuration: &Configuration,
    @@ -436,8 +441,16 @@ where
         })?;
         let span_mode = span_mode(configuration);
     
    +    let decompression = ServiceBuilder::new()
    +        .layer(HandleErrorLayer::<_, ()>::new(decompression_error))
    +        .layer(
    +            tower_http::decompression::RequestDecompressionLayer::new()
    +                .br(true)
    +                .gzip(true)
    +                .deflate(true),
    +        );
         let mut main_route = main_router::<RF>(configuration)
    -        .layer(middleware::from_fn(decompress_request_body))
    +        .layer(decompression)
             .layer(middleware::from_fn_with_state(
                 (license, Instant::now(), Arc::new(AtomicU64::new(0))),
                 license_handler,
    @@ -530,19 +543,21 @@ async fn license_handler<B>(
         }
     }
     
    -pub(super) fn main_router<RF>(configuration: &Configuration) -> axum::Router
    +pub(super) fn main_router<RF>(
    +    configuration: &Configuration,
    +) -> axum::Router<(), DecompressionBody<Body>>
     where
         RF: RouterFactory,
     {
         let mut router = Router::new().route(
             &configuration.supergraph.sanitized_path(),
             get({
    -            move |Extension(service): Extension<RF>, request: Request<Body>| {
    +            move |Extension(service): Extension<RF>, request: Request<DecompressionBody<Body>>| {
                     handle_graphql(service.create().boxed(), request)
                 }
             })
             .post({
    -            move |Extension(service): Extension<RF>, request: Request<Body>| {
    +            move |Extension(service): Extension<RF>, request: Request<DecompressionBody<Body>>| {
                     handle_graphql(service.create().boxed(), request)
                 }
             }),
    @@ -552,12 +567,14 @@ where
             router = router.route(
                 "/",
                 get({
    -                move |Extension(service): Extension<RF>, request: Request<Body>| {
    +                move |Extension(service): Extension<RF>,
    +                      request: Request<DecompressionBody<Body>>| {
                         handle_graphql(service.create().boxed(), request)
                     }
                 })
                 .post({
    -                move |Extension(service): Extension<RF>, request: Request<Body>| {
    +                move |Extension(service): Extension<RF>,
    +                      request: Request<DecompressionBody<Body>>| {
                         handle_graphql(service.create().boxed(), request)
                     }
                 }),
    @@ -569,10 +586,14 @@ where
     
     async fn handle_graphql(
         service: router::BoxService,
    -    http_request: Request<Body>,
    +    http_request: Request<DecompressionBody<Body>>,
     ) -> impl IntoResponse {
         let _guard = SessionCountGuard::start();
     
    +    let (parts, body) = http_request.into_parts();
    +
    +    let http_request = http::Request::from_parts(parts, Body::wrap_stream(BodyStream::new(body)));
    +
         let request: router::Request = http_request.into();
         let context = request.context.clone();
         let accept_encoding = request
    
  • apollo-router/src/axum_factory/utils.rs+0 88 modified
    @@ -2,19 +2,8 @@
     
     use std::net::SocketAddr;
     
    -use async_compression::tokio::write::BrotliDecoder;
    -use async_compression::tokio::write::GzipDecoder;
    -use async_compression::tokio::write::ZlibDecoder;
    -use axum::http::StatusCode;
    -use axum::middleware::Next;
    -use axum::response::*;
    -use futures::prelude::*;
    -use http::header::CONTENT_ENCODING;
    -use http::Request;
    -use hyper::Body;
     use opentelemetry::global;
     use opentelemetry::trace::TraceContextExt;
    -use tokio::io::AsyncWriteExt;
     use tower_http::trace::MakeSpan;
     use tower_service::Service;
     use tracing::Span;
    @@ -26,83 +15,6 @@ use crate::uplink::license_enforcement::LICENSE_EXPIRED_SHORT_MESSAGE;
     
     pub(crate) const REQUEST_SPAN_NAME: &str = "request";
     
    -pub(super) async fn decompress_request_body(
    -    req: Request<Body>,
    -    next: Next<Body>,
    -) -> Result<Response, Response> {
    -    let (parts, body) = req.into_parts();
    -    let content_encoding = parts.headers.get(&CONTENT_ENCODING);
    -    macro_rules! decode_body {
    -        ($decoder: ident, $error_message: expr) => {{
    -            let body_bytes = hyper::body::to_bytes(body)
    -                .map_err(|err| {
    -                    (
    -                        StatusCode::BAD_REQUEST,
    -                        format!("cannot read request body: {err}"),
    -                    )
    -                        .into_response()
    -                })
    -                .await?;
    -            let mut decoder = $decoder::new(Vec::new());
    -            decoder.write_all(&body_bytes).await.map_err(|err| {
    -                (
    -                    StatusCode::BAD_REQUEST,
    -                    format!("{}: {err}", $error_message),
    -                )
    -                    .into_response()
    -            })?;
    -            decoder.shutdown().await.map_err(|err| {
    -                (
    -                    StatusCode::BAD_REQUEST,
    -                    format!("{}: {err}", $error_message),
    -                )
    -                    .into_response()
    -            })?;
    -
    -            Ok(next
    -                .run(Request::from_parts(parts, Body::from(decoder.into_inner())))
    -                .await)
    -        }};
    -    }
    -
    -    match content_encoding {
    -        Some(content_encoding) => match content_encoding.to_str() {
    -            Ok(content_encoding_str) => match content_encoding_str {
    -                "br" => decode_body!(BrotliDecoder, "cannot decompress (brotli) request body"),
    -                "gzip" => decode_body!(GzipDecoder, "cannot decompress (gzip) request body"),
    -                "deflate" => decode_body!(ZlibDecoder, "cannot decompress (deflate) request body"),
    -                "identity" => Ok(next.run(Request::from_parts(parts, body)).await),
    -                unknown => {
    -                    let message = format!("unknown content-encoding header value {unknown:?}");
    -                    tracing::error!(message);
    -                    u64_counter!(
    -                        "apollo_router_http_requests_total",
    -                        "Total number of HTTP requests made.",
    -                        1,
    -                        status = StatusCode::BAD_REQUEST.as_u16() as i64,
    -                        error = message.clone()
    -                    );
    -
    -                    Err((StatusCode::BAD_REQUEST, message).into_response())
    -                }
    -            },
    -
    -            Err(err) => {
    -                let message = format!("cannot read content-encoding header: {err}");
    -                u64_counter!(
    -                    "apollo_router_http_requests_total",
    -                    "Total number of HTTP requests made.",
    -                    1,
    -                    status = 400,
    -                    error = message.clone()
    -                );
    -                Err((StatusCode::BAD_REQUEST, message).into_response())
    -            }
    -        },
    -        None => Ok(next.run(Request::from_parts(parts, body)).await),
    -    }
    -}
    -
     #[derive(Clone, Default)]
     pub(crate) struct PropagatingMakeSpan {
         pub(crate) license: LicenseState,
    
  • apollo-router/src/services/http/service.rs+7 0 modified
    @@ -391,6 +391,13 @@ pin_project! {
         }
     }
     
    +impl<B: hyper::body::HttpBody> BodyStream<B> {
    +    /// Create a new `BodyStream`.
    +    pub(crate) fn new(body: DecompressionBody<B>) -> Self {
    +        Self { inner: body }
    +    }
    +}
    +
     impl<B> Stream for BodyStream<B>
     where
         B: hyper::body::HttpBody,
    
  • apollo-router/src/services/layers/content_negotiation.rs+11 0 modified
    @@ -71,6 +71,17 @@ where
                                 .to_string(),
                             ))
                             .expect("cannot fail");
    +                    u64_counter!(
    +                        "apollo_router_http_requests_total",
    +                        "Total number of HTTP requests made.",
    +                        1,
    +                        status = StatusCode::UNSUPPORTED_MEDIA_TYPE.as_u16() as i64,
    +                        error = format!(
    +                            r#"'content-type' header must be one of: {:?} or {:?}"#,
    +                            APPLICATION_JSON.essence_str(),
    +                            GRAPHQL_JSON_RESPONSE_HEADER_VALUE,
    +                        )
    +                    );
     
                         return Ok(ControlFlow::Break(response.into()));
                     }
    
  • apollo-router/tests/common.rs+6 5 modified
    @@ -10,7 +10,6 @@ use std::time::SystemTime;
     
     use buildstructor::buildstructor;
     use http::header::ACCEPT;
    -use http::header::CONTENT_ENCODING;
     use http::header::CONTENT_TYPE;
     use http::HeaderValue;
     use jsonpath_lib::Selector;
    @@ -378,7 +377,7 @@ impl IntegrationTest {
         }
     
         #[allow(dead_code)]
    -    pub fn execute_bad_content_encoding(
    +    pub fn execute_bad_content_type(
             &self,
         ) -> impl std::future::Future<Output = (String, reqwest::Response)> {
             self.execute_query_internal(&json!({"garbage":{}}), Some("garbage"))
    @@ -387,7 +386,7 @@ impl IntegrationTest {
         fn execute_query_internal(
             &self,
             query: &Value,
    -        content_encoding: Option<&'static str>,
    +        content_type: Option<&'static str>,
         ) -> impl std::future::Future<Output = (String, reqwest::Response)> {
             assert!(
                 self.router.is_some(),
    @@ -404,8 +403,10 @@ impl IntegrationTest {
     
                     let mut request = client
                         .post("http://localhost:4000")
    -                    .header(CONTENT_TYPE, APPLICATION_JSON.essence_str())
    -                    .header(CONTENT_ENCODING, content_encoding.unwrap_or("identity"))
    +                    .header(
    +                        CONTENT_TYPE,
    +                        content_type.unwrap_or(APPLICATION_JSON.essence_str()),
    +                    )
                         .header("apollographql-client-name", "custom_name")
                         .header("apollographql-client-version", "1.0")
                         .header("x-my-header", "test")
    
  • apollo-router/tests/telemetry/metrics.rs+3 2 modified
    @@ -135,10 +135,11 @@ async fn test_bad_queries() {
                 None,
             )
             .await;
    -    router.execute_bad_content_encoding().await;
    +    router.execute_bad_content_type().await;
    +
         router
                 .assert_metrics_contains(
    -                r#"apollo_router_http_requests_total{error="unknown content-encoding header value \"garbage\"",status="400",otel_scope_name="apollo/router"}"#,
    +                r#"apollo_router_http_requests_total{error="'content-type' header must be one of: \"application/json\" or \"application/graphql-response+json\"",status="415",otel_scope_name="apollo/router"}"#,
                     None,
                 )
                 .await;
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

3

News mentions

0

No linked articles in our index yet.