CVE-2024-28101 (Rust, published 2024-03-21T02:52Z)
Description: value: 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. 

Severity: nan (CVSS v3 base score: )
Weakness: NVD-CWE-noinfo \u2014 Insufficient Information
There is insufficient information about the issue to classify it; details are unkown or unspecified.
Repository: https://github.com/apollographql/router
Files changed:
apollo-router/src/axum_factory/axum_http_server_factory.rs
apollo-router/src/axum_factory/utils.rs
apollo-router/src/services/http/service.rs
apollo-router/src/services/layers/content_negotiation.rs
apollo-router/tests/common.rs
apollo-router/tests/telemetry/metrics.rs
Commit message: streaming body decompression

Client request body was fully loaded in memory before decompressing, we
are now decompressing it as it goes


Vulnerable code:
use super::utils::decompress_request_body;
                            response: http::Response::builder()
                                .status(status_code)
                                .body::<hyper::Body>(
                                    serde_json::to_vec(&health).map_err(BoxError::from)?.into(),
                                )?,
        .layer(middleware::from_fn(decompress_request_body))
pub(super) fn main_router<RF>(configuration: &Configuration) -> axum::Router
            move |Extension(service): Extension<RF>, request: Request<Body>| {
            move |Extension(service): Extension<RF>, request: Request<Body>| {
                move |Extension(service): Extension<RF>, request: Request<Body>| {
                move |Extension(service): Extension<RF>, request: Request<Body>| {
    http_request: Request<Body>,
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 tokio::io::AsyncWriteExt;
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),
    }
}

use http::header::CONTENT_ENCODING;
    pub fn execute_bad_content_encoding(
        content_encoding: Option<&'static str>,
                    .header(CONTENT_TYPE, APPLICATION_JSON.essence_str())
                    .header(CONTENT_ENCODING, content_encoding.unwrap_or("identity"))
    router.execute_bad_content_encoding().await;
                r#"apollo_router_http_requests_total{error="unknown content-encoding header value \"garbage\"",status="400",otel_scope_name="apollo/router"}"#,

Fixed code:
use axum::error_handling::HandleErrorLayer;
use tower::ServiceBuilder;
use tower_http::decompression::DecompressionBody;
use crate::services::http::service::BodyStream;
                            response: http::Response::builder().status(status_code).body::<Body>(
                                serde_json::to_vec(&health).map_err(BoxError::from)?.into(),
                            )?,
async fn decompression_error(_error: BoxError) -> axum::response::Response {
    (StatusCode::BAD_REQUEST, "cannot decompress request body").into_response()
}

    let decompression = ServiceBuilder::new()
        .layer(HandleErrorLayer::<_, ()>::new(decompression_error))
        .layer(
            tower_http::decompression::RequestDecompressionLayer::new()
                .br(true)
                .gzip(true)
                .deflate(true),
        );
        .layer(decompression)
pub(super) fn main_router<RF>(
    configuration: &Configuration,
) -> axum::Router<(), DecompressionBody<Body>>
            move |Extension(service): Extension<RF>, request: Request<DecompressionBody<Body>>| {
            move |Extension(service): Extension<RF>, request: Request<DecompressionBody<Body>>| {
                move |Extension(service): Extension<RF>,
                      request: Request<DecompressionBody<Body>>| {
                move |Extension(service): Extension<RF>,
                      request: Request<DecompressionBody<Body>>| {
    http_request: Request<DecompressionBody<Body>>,
    let (parts, body) = http_request.into_parts();

    let http_request = http::Request::from_parts(parts, Body::wrap_stream(BodyStream::new(body)));

impl<B: hyper::body::HttpBody> BodyStream<B> {
    /// Create a new `BodyStream`.
    pub(crate) fn new(body: DecompressionBody<B>) -> Self {
        Self { inner: body }
    }
}

                    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,
                        )
                    );
    pub fn execute_bad_content_type(
        content_type: Option<&'static str>,
                    .header(
                        CONTENT_TYPE,
                        content_type.unwrap_or(APPLICATION_JSON.essence_str()),
                    )
    router.execute_bad_content_type().await;

                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"}"#,