Skip to content

Commit e5ca352

Browse files
committed
Use axum::DefaultBodyLimit for request payload size limiting
The `DefaultBodyLimit` is 2 MB by default but can be adjusted per route. This commit changes our code to use the default limit, except for the publish endpoint which uses a custom 128 MB limit, like before.
1 parent b76f361 commit e5ca352

File tree

6 files changed

+41
-66
lines changed

6 files changed

+41
-66
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

conduit-axum/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ rust-version = "1.56.0"
1212
axum = "=0.6.2"
1313
hyper = { version = "=0.14.23", features = ["server", "stream"] }
1414
http = "=0.2.8"
15+
http-body = "=0.4.5"
1516
percent-encoding = "=2.2.0"
1617
sentry-core = { version = "=0.29.1", features = ["client"] }
1718
thiserror = "=1.0.38"

conduit-axum/src/conduit.rs

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1-
use axum::async_trait;
21
use axum::body::Bytes;
32
use axum::extract::FromRequest;
3+
use axum::response::IntoResponse;
4+
use axum::{async_trait, RequestExt};
5+
use http_body::LengthLimitError;
46
use hyper::Body;
57
use std::error::Error;
68
use std::io::Cursor;
79
use std::ops::{Deref, DerefMut};
810

9-
use crate::fallback::check_content_length;
1011
use crate::response::AxumResponse;
1112
use crate::server_error_response;
1213
pub use http::{header, Extensions, HeaderMap, Method, Request, Response, StatusCode, Uri};
@@ -54,16 +55,31 @@ where
5455
type Rejection = AxumResponse;
5556

5657
async fn from_request(req: Request<Body>, _state: &S) -> Result<Self, Self::Rejection> {
57-
check_content_length(&req)?;
58+
let request = match req.with_limited_body() {
59+
Ok(req) => {
60+
let (parts, body) = req.into_parts();
5861

59-
let (parts, body) = req.into_parts();
62+
let bytes = hyper::body::to_bytes(body).await.map_err(|err| {
63+
if err.downcast_ref::<LengthLimitError>().is_some() {
64+
StatusCode::BAD_REQUEST.into_response()
65+
} else {
66+
server_error_response(&*err)
67+
}
68+
})?;
6069

61-
let full_body = match hyper::body::to_bytes(body).await {
62-
Ok(body) => body,
63-
Err(err) => return Err(server_error_response(&err)),
70+
Request::from_parts(parts, bytes)
71+
}
72+
Err(req) => {
73+
let (parts, body) = req.into_parts();
74+
75+
let bytes = hyper::body::to_bytes(body)
76+
.await
77+
.map_err(|err| server_error_response(&err))?;
78+
79+
Request::from_parts(parts, bytes)
80+
}
6481
};
6582

66-
let request = Request::from_parts(parts, Cursor::new(full_body));
67-
Ok(ConduitRequest(request))
83+
Ok(ConduitRequest(request.map(Cursor::new)))
6884
}
6985
}

conduit-axum/src/fallback.rs

Lines changed: 1 addition & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,10 @@ use crate::response::AxumResponse;
33

44
use std::error::Error;
55

6-
use axum::body::{Body, HttpBody};
76
use axum::extract::Extension;
87
use axum::response::IntoResponse;
9-
use http::header::CONTENT_LENGTH;
108
use http::StatusCode;
11-
use hyper::Request;
12-
use tracing::{error, warn};
13-
14-
/// The maximum size allowed in the `Content-Length` header
15-
///
16-
/// Chunked requests may grow to be larger over time if that much data is actually sent.
17-
/// See the usage section of the README if you plan to use this server in production.
18-
const MAX_CONTENT_LENGTH: u64 = 128 * 1024 * 1024; // 128 MB
9+
use tracing::error;
1910

2011
#[derive(Clone, Debug)]
2112
pub struct ErrorField(pub String);
@@ -42,42 +33,3 @@ pub fn server_error_response<E: Error + ?Sized>(error: &E) -> AxumResponse {
4233
)
4334
.into_response()
4435
}
45-
46-
/// Check for `Content-Length` values that are invalid or too large
47-
///
48-
/// If a `Content-Length` is provided then `hyper::body::to_bytes()` may try to allocate a buffer
49-
/// of this size upfront, leading to a process abort and denial of service to other clients.
50-
///
51-
/// This only checks for requests that claim to be too large. If the request is chunked then it
52-
/// is possible to allocate larger chunks of memory over time, by actually sending large volumes of
53-
/// data. Request sizes must be limited higher in the stack to protect against this type of attack.
54-
pub(crate) fn check_content_length(request: &Request<Body>) -> Result<(), AxumResponse> {
55-
fn bad_request(message: &str) -> AxumResponse {
56-
warn!("Bad request: Content-Length {}", message);
57-
StatusCode::BAD_REQUEST.into_response()
58-
}
59-
60-
if let Some(content_length) = request.headers().get(CONTENT_LENGTH) {
61-
let content_length = match content_length.to_str() {
62-
Ok(some) => some,
63-
Err(_) => return Err(bad_request("not ASCII")),
64-
};
65-
66-
let content_length = match content_length.parse::<u64>() {
67-
Ok(some) => some,
68-
Err(_) => return Err(bad_request("not a u64")),
69-
};
70-
71-
if content_length > MAX_CONTENT_LENGTH {
72-
return Err(bad_request("too large"));
73-
}
74-
}
75-
76-
// A duplicate check, aligning with the specific impl of `hyper::body::to_bytes`
77-
// (at the time of this writing)
78-
if request.size_hint().lower() > MAX_CONTENT_LENGTH {
79-
return Err(bad_request("size_hint().lower() too large"));
80-
}
81-
82-
Ok(())
83-
}

conduit-axum/src/tests.rs

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use crate::{server_error_response, spawn_blocking, ConduitRequest, HandlerResult, ServiceError};
2+
use axum::extract::DefaultBodyLimit;
23
use axum::response::IntoResponse;
34
use axum::Router;
45
use http::header::HeaderName;
@@ -111,7 +112,9 @@ async fn spawn_http_server() -> (
111112
let (quit_tx, quit_rx) = oneshot::channel::<()>();
112113
let addr = ([127, 0, 0, 1], 0).into();
113114

114-
let router = Router::new().fallback(ok_result);
115+
let router = Router::new()
116+
.fallback(ok_result)
117+
.layer(DefaultBodyLimit::max(4096));
115118
let make_service = router.into_make_service();
116119
let server = hyper::Server::bind(&addr).serve(make_service);
117120

@@ -125,8 +128,7 @@ async fn spawn_http_server() -> (
125128

126129
#[tokio::test]
127130
async fn content_length_too_large() {
128-
const ACTUAL_BODY_SIZE: usize = 10_000;
129-
const CLAIMED_CONTENT_LENGTH: u64 = 11_111_111_111_111_111_111;
131+
const ACTUAL_BODY_SIZE: usize = 4097;
130132

131133
let (url, server, quit_tx) = spawn_http_server().await;
132134

@@ -136,10 +138,7 @@ async fn content_length_too_large() {
136138
.send_data(vec![0; ACTUAL_BODY_SIZE].into())
137139
.await
138140
.unwrap();
139-
let req = hyper::Request::put(url)
140-
.header(hyper::header::CONTENT_LENGTH, CLAIMED_CONTENT_LENGTH)
141-
.body(body)
142-
.unwrap();
141+
let req = hyper::Request::put(url).body(body).unwrap();
143142

144143
let resp = client
145144
.request(req)

src/router.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use axum::extract::DefaultBodyLimit;
12
use axum::response::IntoResponse;
23
use axum::routing::{delete, get, post, put};
34
use axum::Router;
@@ -7,12 +8,17 @@ use crate::controllers::*;
78
use crate::util::errors::not_found;
89
use crate::Env;
910

11+
const MAX_PUBLISH_CONTENT_LENGTH: usize = 128 * 1024 * 1024; // 128 MB
12+
1013
pub fn build_axum_router(state: AppState) -> Router {
1114
let mut router = Router::new()
1215
// Route used by both `cargo search` and the frontend
1316
.route("/api/v1/crates", get(krate::search::search))
1417
// Routes used by `cargo`
15-
.route("/api/v1/crates/new", put(krate::publish::publish))
18+
.route(
19+
"/api/v1/crates/new",
20+
put(krate::publish::publish).layer(DefaultBodyLimit::max(MAX_PUBLISH_CONTENT_LENGTH)),
21+
)
1622
.route(
1723
"/api/v1/crates/:crate_id/owners",
1824
get(krate::owners::owners)

0 commit comments

Comments
 (0)