Skip to content

Commit 20d2a20

Browse files
authored
Add middleware for header count and header list item limits in limits plugin (#8285)
2 parents 2576bce + 6a6a4ec commit 20d2a20

File tree

6 files changed

+498
-1
lines changed

6 files changed

+498
-1
lines changed
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
limits:
2+
http_max_request_headers: 5
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
limits:
2+
http_max_header_list_items: 3

apollo-router/src/plugins/limits/layer.rs

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,14 @@ pub(super) enum BodyLimitError {
1818
PayloadTooLarge,
1919
}
2020

21+
#[derive(thiserror::Error, Debug, Display)]
22+
pub(super) enum HeaderLimitError {
23+
/// Request header fields too many
24+
TooManyHeaders,
25+
/// Request header list too many items
26+
TooManyHeaderListItems,
27+
}
28+
2129
struct BodyLimitControlInner {
2230
limit: AtomicUsize,
2331
current: AtomicUsize,
@@ -416,3 +424,113 @@ mod test {
416424
}
417425
}
418426
}
427+
428+
/// Layer that limits the number of headers in an HTTP request
429+
pub(crate) struct RequestHeaderCountLimitLayer {
430+
max_headers: Option<usize>,
431+
}
432+
433+
impl RequestHeaderCountLimitLayer {
434+
pub(crate) fn new(max_headers: Option<usize>) -> Self {
435+
Self { max_headers }
436+
}
437+
}
438+
439+
impl<S> Layer<S> for RequestHeaderCountLimitLayer {
440+
type Service = RequestHeaderCountLimit<S>;
441+
442+
fn layer(&self, inner: S) -> Self::Service {
443+
RequestHeaderCountLimit::new(inner, self.max_headers)
444+
}
445+
}
446+
447+
pub(crate) struct RequestHeaderCountLimit<S> {
448+
inner: S,
449+
max_headers: Option<usize>,
450+
}
451+
452+
impl<S> RequestHeaderCountLimit<S> {
453+
fn new(inner: S, max_headers: Option<usize>) -> Self {
454+
Self { inner, max_headers }
455+
}
456+
}
457+
458+
impl<ReqBody, RespBody, S> Service<http::Request<ReqBody>> for RequestHeaderCountLimit<S>
459+
where
460+
S: Service<http::Request<ReqBody>, Response = http::Response<RespBody>>,
461+
S::Error: From<HeaderLimitError>,
462+
{
463+
type Response = S::Response;
464+
type Error = S::Error;
465+
type Future = futures::future::Either<futures::future::Ready<Result<S::Response, S::Error>>, S::Future>;
466+
467+
fn poll_ready(&mut self, cx: &mut std::task::Context<'_>) -> Poll<Result<(), Self::Error>> {
468+
self.inner.poll_ready(cx)
469+
}
470+
471+
fn call(&mut self, req: http::Request<ReqBody>) -> Self::Future {
472+
if let Some(max_headers) = self.max_headers {
473+
if req.headers().len() > max_headers {
474+
// This will be converted to the appropriate response by the error handling
475+
return futures::future::Either::Left(futures::future::ready(Err(HeaderLimitError::TooManyHeaders.into())));
476+
}
477+
}
478+
futures::future::Either::Right(self.inner.call(req))
479+
}
480+
}
481+
482+
/// Layer that limits the number of items in header lists (for headers with multiple values)
483+
pub(crate) struct RequestHeaderListItemsLimitLayer {
484+
max_items: Option<usize>,
485+
}
486+
487+
impl RequestHeaderListItemsLimitLayer {
488+
pub(crate) fn new(max_items: Option<usize>) -> Self {
489+
Self { max_items }
490+
}
491+
}
492+
493+
impl<S> Layer<S> for RequestHeaderListItemsLimitLayer {
494+
type Service = RequestHeaderListItemsLimit<S>;
495+
496+
fn layer(&self, inner: S) -> Self::Service {
497+
RequestHeaderListItemsLimit::new(inner, self.max_items)
498+
}
499+
}
500+
501+
pub(crate) struct RequestHeaderListItemsLimit<S> {
502+
inner: S,
503+
max_items: Option<usize>,
504+
}
505+
506+
impl<S> RequestHeaderListItemsLimit<S> {
507+
fn new(inner: S, max_items: Option<usize>) -> Self {
508+
Self { inner, max_items }
509+
}
510+
}
511+
512+
impl<ReqBody, RespBody, S> Service<http::Request<ReqBody>> for RequestHeaderListItemsLimit<S>
513+
where
514+
S: Service<http::Request<ReqBody>, Response = http::Response<RespBody>>,
515+
S::Error: From<HeaderLimitError>,
516+
{
517+
type Response = S::Response;
518+
type Error = S::Error;
519+
type Future = futures::future::Either<futures::future::Ready<Result<S::Response, S::Error>>, S::Future>;
520+
521+
fn poll_ready(&mut self, cx: &mut std::task::Context<'_>) -> Poll<Result<(), Self::Error>> {
522+
self.inner.poll_ready(cx)
523+
}
524+
525+
fn call(&mut self, req: http::Request<ReqBody>) -> Self::Future {
526+
if let Some(max_items) = self.max_items {
527+
for (header_name, _) in req.headers().iter() {
528+
let header_values_count = req.headers().get_all(header_name).iter().count();
529+
if header_values_count > max_items {
530+
return futures::future::Either::Left(futures::future::ready(Err(HeaderLimitError::TooManyHeaderListItems.into())));
531+
}
532+
}
533+
}
534+
futures::future::Either::Right(self.inner.call(req))
535+
}
536+
}

apollo-router/src/plugins/limits/mod.rs

Lines changed: 175 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@ use crate::layers::ServiceBuilderExt;
1919
use crate::plugin::Plugin;
2020
use crate::plugin::PluginInit;
2121
use crate::plugins::limits::layer::BodyLimitError;
22+
use crate::plugins::limits::layer::HeaderLimitError;
2223
use crate::plugins::limits::layer::RequestBodyLimitLayer;
24+
use crate::plugins::limits::layer::RequestHeaderCountLimitLayer;
25+
use crate::plugins::limits::layer::RequestHeaderListItemsLimitLayer;
2326
use crate::services::router;
2427
use crate::services::router::BoxService;
2528

@@ -116,6 +119,20 @@ pub(crate) struct Config {
116119
#[schemars(with = "Option<String>", default)]
117120
pub(crate) http1_max_request_buf_size: Option<ByteSize>,
118121

122+
/// Limit the maximum number of headers in an HTTP request.
123+
///
124+
/// If router receives more headers than this limit, it responds to the client with
125+
/// "431 Request Header Fields Too Large".
126+
/// When not specified, no limit is enforced at the middleware level.
127+
pub(crate) http_max_request_headers: Option<usize>,
128+
129+
/// Limit the maximum number of items in a header list (for headers with multiple values).
130+
///
131+
/// If a single header has more values than this limit, it responds to the client with
132+
/// "431 Request Header Fields Too Large".
133+
/// When not specified, no limit is enforced at the middleware level.
134+
pub(crate) http_max_header_list_items: Option<usize>,
135+
119136
/// Limit the depth of nested list fields in introspection queries
120137
/// to protect avoid generating huge responses. Returns a GraphQL
121138
/// error with `{ message: "Maximum introspection depth exceeded" }`
@@ -136,6 +153,8 @@ impl Default for Config {
136153
http_max_request_bytes: 2_000_000,
137154
http1_max_request_headers: None,
138155
http1_max_request_buf_size: None,
156+
http_max_request_headers: None,
157+
http_max_header_list_items: None,
139158
parser_max_tokens: 15_000,
140159

141160
// This is `apollo-parser`’s default, which protects against stack overflow
@@ -174,6 +193,12 @@ impl Plugin for LimitsPlugin {
174193
// Here we need to convert to and from the underlying http request types so that we can use existing middleware.
175194
.map_request(Into::into)
176195
.map_response(Into::into)
196+
.layer(RequestHeaderCountLimitLayer::new(
197+
self.config.http_max_request_headers,
198+
))
199+
.layer(RequestHeaderListItemsLimitLayer::new(
200+
self.config.http_max_header_list_items,
201+
))
177202
.layer(RequestBodyLimitLayer::new(
178203
self.config.http_max_request_bytes,
179204
))
@@ -209,7 +234,10 @@ impl LimitsPlugin {
209234
}
210235

211236
match root_cause.downcast_ref::<BodyLimitError>() {
212-
None => Err(e),
237+
None => match root_cause.downcast_ref::<HeaderLimitError>() {
238+
None => Err(e),
239+
Some(header_error) => Ok(header_error.into_response(ctx)),
240+
},
213241
Some(_) => Ok(BodyLimitError::PayloadTooLarge.into_response(ctx)),
214242
}
215243
}
@@ -236,6 +264,37 @@ impl BodyLimitError {
236264
}
237265
}
238266

267+
impl HeaderLimitError {
268+
fn into_response(&self, ctx: Context) -> router::Response {
269+
match self {
270+
HeaderLimitError::TooManyHeaders => router::Response::error_builder()
271+
.error(
272+
graphql::Error::builder()
273+
.message("Request header fields too many")
274+
.extension_code("INVALID_GRAPHQL_REQUEST")
275+
.extension("details", "Request header fields too many")
276+
.build(),
277+
)
278+
.status_code(StatusCode::REQUEST_HEADER_FIELDS_TOO_LARGE)
279+
.context(ctx)
280+
.build()
281+
.unwrap(),
282+
HeaderLimitError::TooManyHeaderListItems => router::Response::error_builder()
283+
.error(
284+
graphql::Error::builder()
285+
.message("Request header list too many items")
286+
.extension_code("INVALID_GRAPHQL_REQUEST")
287+
.extension("details", "Request header list too many items")
288+
.build(),
289+
)
290+
.status_code(StatusCode::REQUEST_HEADER_FIELDS_TOO_LARGE)
291+
.context(ctx)
292+
.build()
293+
.unwrap(),
294+
}
295+
}
296+
}
297+
239298
register_plugin!("apollo", "limits", LimitsPlugin);
240299

241300
#[cfg(test)]
@@ -430,6 +489,121 @@ mod test {
430489
);
431490
}
432491

492+
#[tokio::test]
493+
async fn test_header_count_limit_exceeded() {
494+
let plugin = header_count_plugin().await;
495+
let resp = plugin
496+
.router_service(|_| async { panic!("should have rejected request") })
497+
.call(
498+
router::Request::fake_builder()
499+
.header("header1", "value1")
500+
.header("header2", "value2")
501+
.header("header3", "value3")
502+
.header("header4", "value4")
503+
.header("header5", "value5")
504+
.header("header6", "value6") // This should exceed the limit of 5
505+
.body(router::body::empty())
506+
.build()
507+
.unwrap(),
508+
)
509+
.await;
510+
assert!(resp.is_ok());
511+
let resp = resp.unwrap();
512+
assert_eq!(resp.response.status(), StatusCode::REQUEST_HEADER_FIELDS_TOO_LARGE);
513+
let body_str = String::from_utf8(
514+
router::body::into_bytes(resp.response.into_body())
515+
.await
516+
.unwrap()
517+
.to_vec()
518+
).unwrap();
519+
assert!(body_str.contains("Request header fields too many"));
520+
}
521+
522+
#[tokio::test]
523+
async fn test_header_count_limit_ok() {
524+
let plugin = header_count_plugin().await;
525+
let resp = plugin
526+
.router_service(|_| async { Ok(router::Response::fake_builder().build().unwrap()) })
527+
.call(
528+
router::Request::fake_builder()
529+
.header("header1", "value1")
530+
.header("header2", "value2")
531+
.header("header3", "value3")
532+
.body(router::body::empty())
533+
.build()
534+
.unwrap(),
535+
)
536+
.await;
537+
assert!(resp.is_ok());
538+
let resp = resp.unwrap();
539+
assert_eq!(resp.response.status(), StatusCode::OK);
540+
}
541+
542+
#[tokio::test]
543+
async fn test_header_list_items_limit_exceeded() {
544+
let plugin = header_list_items_plugin().await;
545+
let mut request = router::Request::fake_builder()
546+
.body(router::body::empty());
547+
548+
// Create a request with a header that has 4 values (exceeds limit of 3)
549+
for i in 1..=4 {
550+
request = request.header("test-header", format!("value{}", i));
551+
}
552+
553+
let resp = plugin
554+
.router_service(|_| async { panic!("should have rejected request") })
555+
.call(request.build().unwrap())
556+
.await;
557+
assert!(resp.is_ok());
558+
let resp = resp.unwrap();
559+
assert_eq!(resp.response.status(), StatusCode::REQUEST_HEADER_FIELDS_TOO_LARGE);
560+
let body_str = String::from_utf8(
561+
router::body::into_bytes(resp.response.into_body())
562+
.await
563+
.unwrap()
564+
.to_vec()
565+
).unwrap();
566+
assert!(body_str.contains("Request header list too many items"));
567+
}
568+
569+
#[tokio::test]
570+
async fn test_header_list_items_limit_ok() {
571+
let plugin = header_list_items_plugin().await;
572+
let mut request = router::Request::fake_builder()
573+
.body(router::body::empty());
574+
575+
// Create a request with a header that has 2 values (within limit of 3)
576+
for i in 1..=2 {
577+
request = request.header("test-header", format!("value{}", i));
578+
}
579+
580+
let resp = plugin
581+
.router_service(|_| async { Ok(router::Response::fake_builder().build().unwrap()) })
582+
.call(request.build().unwrap())
583+
.await;
584+
assert!(resp.is_ok());
585+
let resp = resp.unwrap();
586+
assert_eq!(resp.response.status(), StatusCode::OK);
587+
}
588+
589+
async fn header_count_plugin() -> PluginTestHarness<LimitsPlugin> {
590+
let plugin: PluginTestHarness<LimitsPlugin> = PluginTestHarness::builder()
591+
.config(include_str!("fixtures/header_count_limit.router.yaml"))
592+
.build()
593+
.await
594+
.expect("test harness");
595+
plugin
596+
}
597+
598+
async fn header_list_items_plugin() -> PluginTestHarness<LimitsPlugin> {
599+
let plugin: PluginTestHarness<LimitsPlugin> = PluginTestHarness::builder()
600+
.config(include_str!("fixtures/header_list_items_limit.router.yaml"))
601+
.build()
602+
.await
603+
.expect("test harness");
604+
plugin
605+
}
606+
433607
async fn plugin() -> PluginTestHarness<LimitsPlugin> {
434608
let plugin: PluginTestHarness<LimitsPlugin> = PluginTestHarness::builder()
435609
.config(include_str!("fixtures/content_length_limit.router.yaml"))

0 commit comments

Comments
 (0)