max / makenotwork
1 file changed,
+29 insertions,
-6 deletions
| @@ -55,7 +55,7 @@ pub(super) async fn upload_image_handler( | |||
| 55 | 55 | } | |
| 56 | 56 | ||
| 57 | 57 | // Read the multipart field | |
| 58 | - | let field = multipart | |
| 58 | + | let mut field = multipart | |
| 59 | 59 | .next_field() | |
| 60 | 60 | .await | |
| 61 | 61 | .map_err(|e| { | |
| @@ -67,10 +67,33 @@ pub(super) async fn upload_image_handler( | |||
| 67 | 67 | let filename = field.file_name().unwrap_or("image").to_string(); | |
| 68 | 68 | let content_type = field.content_type().unwrap_or("application/octet-stream").to_string(); | |
| 69 | 69 | ||
| 70 | - | let data = field.bytes().await.map_err(|e| { | |
| 71 | - | tracing::error!(error = ?e, "failed to read upload bytes"); | |
| 72 | - | (StatusCode::BAD_REQUEST, "Failed to read file.").into_response() | |
| 73 | - | })?; | |
| 70 | + | // Read the field chunk-by-chunk, bailing the instant we cross MAX_IMAGE_SIZE | |
| 71 | + | // rather than buffering the whole field first (`field.bytes()`) and checking | |
| 72 | + | // afterwards. The `/p/{slug}/upload` route already carries a `DefaultBodyLimit` | |
| 73 | + | // (routes/mod.rs), but that is a separately-configured layer that could drift; | |
| 74 | + | // enforcing the same cap here keeps the handler self-defending against a | |
| 75 | + | // memory-DoS regardless of the routing setup. See audit_review.md (uploads | |
| 76 | + | // buffer-before-cap). | |
| 77 | + | let mut data: Vec<u8> = Vec::new(); | |
| 78 | + | loop { | |
| 79 | + | match field.chunk().await { | |
| 80 | + | Ok(Some(chunk)) => { | |
| 81 | + | if data.len() + chunk.len() > storage::MAX_IMAGE_SIZE { | |
| 82 | + | return Err(( | |
| 83 | + | StatusCode::PAYLOAD_TOO_LARGE, | |
| 84 | + | "Image exceeds the 5 MB limit.", | |
| 85 | + | ) | |
| 86 | + | .into_response()); | |
| 87 | + | } | |
| 88 | + | data.extend_from_slice(&chunk); | |
| 89 | + | } | |
| 90 | + | Ok(None) => break, | |
| 91 | + | Err(e) => { | |
| 92 | + | tracing::error!(error = ?e, "failed to read upload bytes"); | |
| 93 | + | return Err((StatusCode::BAD_REQUEST, "Failed to read file.").into_response()); | |
| 94 | + | } | |
| 95 | + | } | |
| 96 | + | } | |
| 74 | 97 | ||
| 75 | 98 | let (ext, validated_ct) = storage::validate_image(&filename, &content_type, &data) | |
| 76 | 99 | .map_err(|msg| (StatusCode::UNPROCESSABLE_ENTITY, msg).into_response())?; | |
| @@ -79,7 +102,7 @@ pub(super) async fn upload_image_handler( | |||
| 79 | 102 | let data = if ext == "jpg" { | |
| 80 | 103 | storage::strip_exif_jpeg(&data) | |
| 81 | 104 | } else { | |
| 82 | - | data.to_vec() | |
| 105 | + | data | |
| 83 | 106 | }; | |
| 84 | 107 | ||
| 85 | 108 | let s3_key = storage::generate_image_key(&slug, ext); |