Improper Check or Handling of Exceptional Conditions in apollo-router
Description
The Apollo Router is a configurable, high-performance graph router written in Rust to run a federated supergraph that uses Apollo Federation. Affected versions are subject to a Denial-of-Service (DoS) type vulnerability which causes the Router to panic and terminate when a multi-part response is sent. When users send queries to the router that uses the @defer or Subscriptions, the Router will panic. To be vulnerable, users of Router must have a coprocessor with coprocessor.supergraph.response configured in their router.yaml and also to support either @defer or Subscriptions. Apollo Router version 1.33.0 has a fix for this vulnerability which was introduced in PR #4014. Users are advised to upgrade. Users unable to upgrade should avoid using the coprocessor supergraph response or disable defer and subscriptions support and continue to use the coprocessor supergraph response.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Apollo Router panics and terminates on multi-part responses when a coprocessor is configured with @defer or subscriptions, leading to DoS.
Vulnerability
Overview
CVE-2023-45812 is a denial-of-service (DoS) vulnerability in the Apollo Router, a high-performance graph router for Apollo Federation. The bug causes the router to panic and terminate when it processes a multi-part response—specifically, responses generated by @defer directives or GraphQL subscriptions—while a coprocessor is configured with coprocessor.supergraph.response in the router's YAML configuration [1][2]. The root cause lies in how the router's coprocessor handling code interacts with streaming responses; the panic occurs because the router attempts to process a multi-part response in a way that is not compatible with the coprocessor's expected data format [4].
Exploitation
Conditions
To be vulnerable, an attacker must be able to send queries to the router that trigger either @defer or subscriptions. Additionally, the router must have a coprocessor configured with the coprocessor.supergraph.response stage enabled. No authentication is required if the router is publicly accessible; any user who can send GraphQL queries can trigger the panic. The attack does not require special network position beyond the ability to reach the router's endpoint [2].
Impact
Successful exploitation results in the router process panicking and terminating, causing a denial of service. This disrupts all GraphQL traffic served by that router instance until it is restarted. The vulnerability does not lead to data leakage or privilege escalation; the sole impact is service unavailability [2].
Mitigation
Apollo Router version 1.33.0 includes a fix introduced in pull request #4014 [1][4]. Users are advised to upgrade immediately. For those unable to upgrade, the recommended workaround is to either disable the coprocessor supergraph response configuration or disable @defer and subscription support entirely [2]. The fix ensures that the coprocessor correctly handles multi-part responses without panicking [4].
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.
| Package | Affected versions | Patched versions |
|---|---|---|
apollo-routercrates.io | >= 1.31.0, < 1.33.0 | 1.33.0 |
Affected products
2- apollographql/routerv5Range: >= 1.31.0, < 1.33.0
Patches
1b917b8c117b4Fix and add test for co-processors handling of streaming responses. (#4014)
4 files changed · +132 −5
apollo-router/src/plugins/coprocessor/supergraph.rs+88 −5 modified@@ -428,7 +428,7 @@ where // Note: We deliberately DO NOT send headers or status_code even if the user has // requested them. That's because they are meaningless on a deferred response and // providing them will be a source of confusion. - let payload = Externalizable::router_builder() + let payload = Externalizable::supergraph_builder() .stage(PipelineStep::SupergraphResponse) .and_id(TraceId::maybe_new().map(|id| id.to_string())) .and_body(body_to_send) @@ -753,13 +753,13 @@ mod tests { let value = response.headers().get("aheader").unwrap(); - assert_eq!("a value", value); + assert_eq!(value, "a value"); assert_eq!( - "my error message", response.body_mut().next().await.unwrap().errors[0] .message - .as_str() + .as_str(), + "my error message" ); } @@ -852,7 +852,7 @@ mod tests { "this-is-a-test-context": 42 } }, - "sdl": "the sdl shouldnt change" + "sdl": "the sdl shouldn't change" }); Ok(hyper::Response::builder() .body(Body::from(serde_json::to_string(&input).unwrap())) @@ -889,8 +889,91 @@ mod tests { let body = res.response.body_mut().next().await.unwrap(); // the body should have changed: assert_eq!( + serde_json::to_value(&body).unwrap(), json!({ "data": { "test": 42_u32 } }), + ); + } + + #[tokio::test] + async fn defer() { + let supergraph_stage = SupergraphStage { + response: SupergraphResponseConf { + headers: true, + context: true, + body: true, + sdl: true, + status_code: false, + }, + request: Default::default(), + }; + + let mut mock_supergraph_service = MockSupergraphService::new(); + + mock_supergraph_service + .expect_call() + .returning(|req: supergraph::Request| { + Ok(supergraph::Response::fake_stream_builder() + .response( + graphql::Response::builder() + .data(json!({ "test": 1 })) + .has_next(true) + .build(), + ) + .response( + graphql::Response::builder() + .data(json!({ "test": 2 })) + .has_next(false) + .build(), + ) + .context(req.context) + .build() + .unwrap()) + }); + + let mock_http_client = mock_with_deferred_callback(move |res: hyper::Request<Body>| { + Box::pin(async { + let deserialized_response: Externalizable<serde_json::Value> = + serde_json::from_slice(&hyper::body::to_bytes(res.into_body()).await.unwrap()) + .unwrap(); + assert_eq!(EXTERNALIZABLE_VERSION, deserialized_response.version); + assert_eq!( + PipelineStep::SupergraphResponse.to_string(), + deserialized_response.stage + ); + + Ok(hyper::Response::builder() + .body(Body::from( + serde_json::to_string(&deserialized_response).unwrap(), + )) + .unwrap()) + }) + }); + + let service = supergraph_stage.as_service( + mock_http_client, + mock_supergraph_service.boxed(), + "http://test".to_string(), + Arc::new("".to_string()), + ); + + let request = supergraph::Request::canned_builder() + .query("foo") + .build() + .unwrap(); + + let mut res = service.oneshot(request).await.unwrap(); + + let body = res.response.body_mut().next().await.unwrap(); + // the body should have changed: + assert_eq!( + serde_json::to_value(&body).unwrap(), + json!({ "data": { "test": 1 }, "hasNext": true }), + ); + let body = res.response.body_mut().next().await.unwrap(); + // the body should have changed: + assert_eq!( serde_json::to_value(&body).unwrap(), + json!({ "data": { "test": 2 }, "hasNext": false }), ); } }
apollo-router/src/services/external.rs+11 −0 modified@@ -261,6 +261,17 @@ mod test { .build(); } + #[test] + #[should_panic] + fn it_will_not_build_router_externalizable_incorrectl_supergraph() { + Externalizable::<String>::router_builder() + .stage(PipelineStep::SupergraphRequest) + .build(); + Externalizable::<String>::router_builder() + .stage(PipelineStep::SupergraphResponse) + .build(); + } + #[test] fn it_will_build_subgraph_externalizable_correctly() { Externalizable::<String>::subgraph_builder()
apollo-router/src/services/supergraph.rs+28 −0 modified@@ -251,6 +251,34 @@ impl Response { ) } + /// This is the constructor (or builder) to use when constructing a "fake" Response stream. + /// + /// This does not enforce the provision of the data that is required for a fully functional + /// Response. It's usually enough for testing, when a fully constructed Response is + /// difficult to construct and not required for the purposes of the test. + /// + /// In addition, fake responses are expected to be valid, and will panic if given invalid values. + #[builder(visibility = "pub")] + fn fake_stream_new( + responses: Vec<graphql::Response>, + status_code: Option<StatusCode>, + headers: MultiMap<TryIntoHeaderName, TryIntoHeaderValue>, + context: Context, + ) -> Result<Self, BoxError> { + let mut builder = http::Response::builder().status(status_code.unwrap_or(StatusCode::OK)); + for (key, values) in headers { + let header_name: HeaderName = key.try_into()?; + for value in values { + let header_value: HeaderValue = value.try_into()?; + builder = builder.header(header_name.clone(), header_value); + } + } + + let stream = futures::stream::iter(responses); + let response = builder.body(stream.boxed())?; + Ok(Self { response, context }) + } + /// This is the constructor (or builder) to use when constructing a Response that represents a global error. /// It has no path and no response data. /// This is useful for things such as authentication errors.
.changesets/fix_bryn_fix_coprocessor_stream.md+5 −0 added@@ -0,0 +1,5 @@ +### Fix panic when streaming responses to co-processor ([Issue #4013](https://github.com/apollographql/router/issues/4013)) + +Streamed responses will no longer cause a panic in the co-processor plugin. This affected defer and stream queries. + +By [@BrynCooke](https://github.com/BrynCooke) in https://github.com/apollographql/router/pull/4014
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/advisories/GHSA-r344-xw3p-2frjghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2023-45812ghsaADVISORY
- github.com/apollographql/router/commit/b917b8c117b46a2d508428c0856f4927dfcfc341ghsaWEB
- github.com/apollographql/router/issues/4013ghsaWEB
- github.com/apollographql/router/pull/4014ghsax_refsource_MISCWEB
- github.com/apollographql/router/security/advisories/GHSA-r344-xw3p-2frjghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.