ASVS 5.0 V5.2.1 (level 1) requires that the application only accepts files of a size it can process without causing a loss of performance or a denial of service.
Pick a maximum for each upload feature, enforce it before the whole file is read into memory or disk, and reject anything over it with a clear error. Level 1 means every application should do this, regardless of how sensitive it is.
Why it matters
An upload endpoint lets a stranger send you as many bytes as they like. Without a cap, one request can fill a disk, exhaust memory when a framework buffers the body, or tie up a worker for minutes while a slow connection trickles data. A few parallel requests are enough to take down a small service. Image and document processors add CPU cost on top, since a large file takes far longer to scan or convert.
The cap needs to exist at several layers, because each one fails differently if it is the only one.
How to implement it
Set the limit in every layer that touches the body, with the same number or a slightly larger one as you go up.
At the proxy, which stops oversized bodies before they reach your code:
client_max_body_size 5m;
client_body_timeout 15s;
In the application framework. Express with multer:
import multer from 'multer';
const upload = multer({
storage: multer.memoryStorage(),
limits: { fileSize: 5 * 1024 * 1024, files: 1, fields: 10 },
});
app.post('/avatar', upload.single('file'), handler);
In Python with Flask:
app.config['MAX_CONTENT_LENGTH'] = 5 * 1024 * 1024 # returns 413 if exceeded
Do not trust the Content-Length header alone, since a client can lie or use chunked encoding. Count bytes as you stream and abort when the limit is hit. For large legitimate files, stream straight to object storage with a presigned upload that has a size condition, rather than passing the bytes through your servers.
Return 413 with a message that names the limit. Apply a lower limit to unauthenticated endpoints than to authenticated ones, and combine the cap with rate limiting.
Limits for streaming, object storage and background work
For anything above a few megabytes, avoid passing bytes through your application at all. Issue a presigned upload to object storage with conditions that the storage service enforces, and process the file afterward from a queue.
import { S3Client } from '@aws-sdk/client-s3';
import { createPresignedPost } from '@aws-sdk/s3-presigned-post';
export async function signUpload(userId) {
const key = `incoming/${userId}/${crypto.randomUUID()}`;
return createPresignedPost(new S3Client({}), {
Bucket: process.env.BUCKET,
Key: key,
Conditions: [['content-length-range', 1, 10 * 1024 * 1024]],
Expires: 120,
});
}
The size rule runs in the storage service, so even a client that ignores your front end cannot go beyond it. After the upload, a worker validates type and content (see V5.2.2) before the file moves from the incoming area to its final location.
Pay attention to work that follows the upload. A 10 MB spreadsheet can take far longer to parse than a 10 MB image to resize. Set limits per type, set CPU and time budgets on workers, and cap the number of jobs in flight per user. When the queue backs up, shed load with a clear error rather than letting every request wait.
Add lifecycle rules to the incoming area so abandoned and unvalidated files expire after a day. And watch the numbers: track the 95th percentile upload size and the count of 413 responses. A sudden rise in either is worth a look.
How to test it
Generate files just under and just over the cap and check both outcomes.
head -c 5242880 /dev/zero > ok.bin
head -c 5242881 /dev/zero > big.bin
curl -s -o /dev/null -w "%{http_code}\n" -F file=@ok.bin https://example.com/avatar
curl -s -o /dev/null -w "%{http_code}\n" -F file=@big.bin https://example.com/avatar
The first should succeed (or fail for type reasons), and the second must return 413. Then send a body with no Content-Length using chunked transfer, with data well past the limit, and confirm the server stops reading early. Watch memory and disk during a 500 MB attempt: usage must stay flat. Finally test a slow upload at a few bytes per second to confirm the timeout fires.
Common mistakes
- A limit only in client side JavaScript.
- Relying on the Content-Length header and ignoring chunked bodies.
- Buffering the whole upload into memory before checking its size.
- Setting the proxy limit higher than the application limit, or forgetting the proxy limit entirely.
- No timeout, so a slow client holds a worker indefinitely.
- Limiting file size but not the number of fields, parts or files in a multipart request.
Try it yourself
Click through it at your own pace, or open it full screen.
Quick check
Is a client side size check enough?
No. Attackers skip the browser. The server and proxy must enforce the limit while the body streams in.
Sources
- OWASP Application Security Verification Standard, requirement V5.2.1 (level 1)
- OWASP File Upload Cheat Sheet