Limit size of request bodies in Bytes extractor (#1362)

* Limit size of request bodies in `Bytes` extractor (#1346)

* Apply default limit to request body size

* Support disabling the default limit

* docs

* changelog

* fix doc test

* fix docs links

* Avoid unhelpful compiler suggestion (#1251)

Co-authored-by: Jonas Platte <[email protected]>
This commit is contained in:
David Pedersen
2022-09-10 09:55:28 +02:00
committed by GitHub
co-authored by Jonas Platte
parent 3990c3a6eb
commit 95e21c1940
12 changed files with 244 additions and 38 deletions
+101
View File
@@ -0,0 +1,101 @@
use self::private::DefaultBodyLimitService;
use tower_layer::Layer;
/// Layer for configuring the default request body limit.
///
/// For security reasons, [`Bytes`] will, by default, not accept bodies larger than 2MB. This also
/// applies to extractors that uses [`Bytes`] internally such as `String`, [`Json`], and [`Form`].
///
/// This middleware provides ways to configure that.
///
/// Note that if an extractor consumes the body directly with [`Body::data`], or similar, the
/// default limit is _not_ applied.
///
/// [`Body::data`]: http_body::Body::data
/// [`Bytes`]: bytes::Bytes
/// [`Json`]: https://docs.rs/axum/0.5/axum/struct.Json.html
/// [`Form`]: https://docs.rs/axum/0.5/axum/struct.Form.html
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct DefaultBodyLimit;
impl DefaultBodyLimit {
/// Disable the default request body limit.
///
/// This must be used to receive bodies larger than the default limit of 2MB using [`Bytes`] or
/// an extractor built on it such as `String`, [`Json`], [`Form`].
///
/// Note that if you're accepting data from untrusted remotes it is recommend to add your own
/// limit such as [`tower_http::limit`].
///
/// # Example
///
/// ```
/// use axum::{
/// Router,
/// routing::get,
/// body::{Bytes, Body},
/// extract::DefaultBodyLimit,
/// };
/// use tower_http::limit::RequestBodyLimitLayer;
/// use http_body::Limited;
///
/// let app: Router<Limited<Body>> = Router::new()
/// .route("/", get(|body: Bytes| async {}))
/// // Disable the default limit
/// .layer(DefaultBodyLimit::disable())
/// // Set a different limit
/// .layer(RequestBodyLimitLayer::new(10 * 1000 * 1000));
/// ```
///
/// [`tower_http::limit`]: https://docs.rs/tower-http/0.3.4/tower_http/limit/index.html
/// [`Bytes`]: bytes::Bytes
/// [`Json`]: https://docs.rs/axum/0.5/axum/struct.Json.html
/// [`Form`]: https://docs.rs/axum/0.5/axum/struct.Form.html
pub fn disable() -> Self {
Self
}
}
impl<S> Layer<S> for DefaultBodyLimit {
type Service = DefaultBodyLimitService<S>;
fn layer(&self, inner: S) -> Self::Service {
DefaultBodyLimitService { inner }
}
}
#[derive(Copy, Clone, Debug)]
pub(crate) struct DefaultBodyLimitDisabled;
mod private {
use super::DefaultBodyLimitDisabled;
use http::Request;
use std::task::Context;
use tower_service::Service;
#[derive(Debug, Clone, Copy)]
pub struct DefaultBodyLimitService<S> {
pub(super) inner: S,
}
impl<B, S> Service<Request<B>> for DefaultBodyLimitService<S>
where
S: Service<Request<B>>,
{
type Response = S::Response;
type Error = S::Error;
type Future = S::Future;
#[inline]
fn poll_ready(&mut self, cx: &mut Context<'_>) -> std::task::Poll<Result<(), Self::Error>> {
self.inner.poll_ready(cx)
}
#[inline]
fn call(&mut self, mut req: Request<B>) -> Self::Future {
req.extensions_mut().insert(DefaultBodyLimitDisabled);
self.inner.call(req)
}
}
}
+3
View File
@@ -12,9 +12,12 @@ use std::convert::Infallible;
pub mod rejection;
mod default_body_limit;
mod request_parts;
mod tuple;
pub use self::default_body_limit::DefaultBodyLimit;
/// Types that can be created from requests.
///
/// See [`axum::extract`] for more details.
+26 -11
View File
@@ -1,4 +1,6 @@
use super::{rejection::*, FromRequest, RequestParts};
use super::{
default_body_limit::DefaultBodyLimitDisabled, rejection::*, FromRequest, RequestParts,
};
use crate::BoxError;
use async_trait::async_trait;
use bytes::Bytes;
@@ -92,11 +94,22 @@ where
type Rejection = BytesRejection;
async fn from_request(req: &mut RequestParts<B>) -> Result<Self, Self::Rejection> {
// update docs in `axum-core/src/extract/default_body_limit.rs` and
// `axum/src/docs/extract.md` if this changes
const DEFAULT_LIMIT: usize = 2_097_152; // 2 mb
let body = take_body(req)?;
let bytes = crate::body::to_bytes(body)
.await
.map_err(FailedToBufferBody::from_err)?;
let bytes = if req.extensions().get::<DefaultBodyLimitDisabled>().is_some() {
crate::body::to_bytes(body)
.await
.map_err(FailedToBufferBody::from_err)?
} else {
let body = http_body::Limited::new(body, DEFAULT_LIMIT);
crate::body::to_bytes(body)
.await
.map_err(FailedToBufferBody::from_err)?
};
Ok(bytes)
}
@@ -112,14 +125,16 @@ where
type Rejection = StringRejection;
async fn from_request(req: &mut RequestParts<B>) -> Result<Self, Self::Rejection> {
let body = take_body(req)?;
let bytes = Bytes::from_request(req).await.map_err(|err| match err {
BytesRejection::FailedToBufferBody(inner) => StringRejection::FailedToBufferBody(inner),
BytesRejection::BodyAlreadyExtracted(inner) => {
StringRejection::BodyAlreadyExtracted(inner)
}
})?;
let bytes = crate::body::to_bytes(body)
.await
.map_err(FailedToBufferBody::from_err)?
.to_vec();
let string = String::from_utf8(bytes).map_err(InvalidUtf8::from_err)?;
let string = std::str::from_utf8(&bytes)
.map_err(InvalidUtf8::from_err)?
.to_owned();
Ok(string)
}