Web Security (OWASP Top 10:2025) Module 11 – A10:2025 Mishandling of Exceptional Conditions
Resource exhaustion, limits and ReDoS
Stop requests from using more time, memory and connections than they deserve: size and time limits, regex backtracking, decompression bombs and status codes.
What you will learn
- List the limits every service needs: body and header size, JSON depth, page size, timeouts, concurrency and archive sizes
- Configure limits in Node.js and Python servers and in a reverse proxy, and say which defaults leave a limit unset
- Explain catastrophic backtracking and fix a vulnerable regular expression with a rewrite, a cap or a linear-time engine
- Prevent compressed and deeply nested input from causing more work than it deserves, before it is unpacked or parsed
- Answer oversized and overloaded requests with the right 413, 414, 429, 431 or 503 status, and keep other endpoints serving
Before you start
What you’ll learn · Before you start
What you will learn
- List the limits every service needs: body and header size, JSON depth, page size, timeouts, concurrency and archive sizes
- Configure limits in Node.js and Python servers and in a reverse proxy, and say which defaults leave a limit unset
- Explain catastrophic backtracking and fix a vulnerable regular expression with a rewrite, a cap or a linear-time engine
- Prevent compressed and deeply nested input from causing more work than it deserves, before it is unpacked or parsed
- Answer oversized and overloaded requests with the right 413, 414, 429, 431 or 503 status, and keep other endpoints serving
Before you start
On this page
A request that costs the server 400 milliseconds is harmless. A thousand of them in a minute are not, and neither is one text that makes a pattern run for an hour. In this lesson’s model, a regular expression with nested quantifiers needs about 540 million tries on 30 characters, and more than an hour on 40.
OWASP’s A10:2025 says that nothing in information technology should be limitless. This lesson covers resource exhaustion, the limits that prevent it, and ReDoS, a regular expression that takes exponentially longer as its input grows, and it ends with an exercise on hostile input.
Every limit is a decision, and the runtime’s defaults leave several of them undecided
resource exhaustion: A condition in which requests use up something the service shares, such as CPU time, memory, connections, disk or a paid quota, so that it slows down or stops for everyone. CWE-770 names the missing limits behind it. happens when requests use up something the service shares: CPU time, memory, connections, disk or a paid quota. A bug, a retry loop or one large customer causes it as easily as an attacker does. CWE-770 names the cause, allocation of resources without limits or throttling.
Limits are numbers fixed before the request arrives: how much of a resource one request, one user or one kind of work may use. They matter most near capacity, where a server slows down fast. Move the arrivals of this single-queue model from 90 to 99 a second:
At 90 % utilisation a request spends 100 ms in the system on average, 10 times its 10 ms of service: the rest is waiting. Little's law: L = λW = 9 requests in the system.
The numbers as a table
| Utilisation | Mean time in the system |
|---|---|
| 50 % | 20 ms |
| 70 % | 33.3 ms |
| 80 % | 50 ms |
| 90 % | 100 ms |
| 95 % | 200 ms |
| 99 % | 1,000 ms |
At 90 arrivals a second the average request takes 100 ms, ten times its 10 ms of service, and at 99 it takes 1,000 ms, a hundred times. A plain Node.js server decides some limits and leaves others to you. This program reads them from a server that is never started, then reads a body of three megabytes with and without a count:
// The limits of a plain Node.js HTTP server, read from a server object that is never started, and what happens to a
// request body when nothing counts it.
import http from 'node:http';
import { Readable } from 'node:stream';
const server = http.createServer();
const limits = [
['server.headersTimeout', server.headersTimeout, 'ms to receive all the headers of a request'],
['server.requestTimeout', server.requestTimeout, 'ms to receive the whole request'],
['server.keepAliveTimeout', server.keepAliveTimeout, 'ms an idle connection is kept for the next request'],
['server.timeout', server.timeout, 'ms of socket inactivity (0: never)'],
['server.maxRequestsPerSocket', server.maxRequestsPerSocket, 'requests on one connection (0: no limit)'],
['http.maxHeaderSize', http.maxHeaderSize, 'bytes of headers'],
];
for (const [name, value, meaning] of limits) console.log(`${name.padEnd(28)} ${String(value).padStart(7)} ${meaning}`);
console.log('A setting for the size of a request body: none. The server hands your code a stream.\n');
// A body of 3 chunks of 1 MiB arrives. The first handler reads it all; the second counts as it reads.
const MiB = 1024 * 1024;
const body = () => Readable.from([Buffer.alloc(MiB), Buffer.alloc(MiB), Buffer.alloc(MiB)]);
async function readAll(stream) {
let size = 0;
for await (const chunk of stream) size += chunk.length;
return size;
}
async function readUpTo(stream, limit) {
let size = 0;
for await (const chunk of stream) {
size += chunk.length;
if (size > limit) {
stream.destroy();
return { status: 413, bytesRead: size };
}
}
return { status: 200, bytesRead: size };
}
console.log(`Without a limit the handler read ${(await readAll(body())) / MiB} MiB.`);
const stopped = await readUpTo(body(), MiB);
console.log(`With a limit of 1 MiB it answered ${stopped.status} after reading ${stopped.bytesRead / MiB} MiB.`); Output
server.headersTimeout 60000 ms to receive all the headers of a request server.requestTimeout 300000 ms to receive the whole request server.keepAliveTimeout 5000 ms an idle connection is kept for the next request server.timeout 0 ms of socket inactivity (0: never) server.maxRequestsPerSocket 0 requests on one connection (0: no limit) http.maxHeaderSize 16384 bytes of headers A setting for the size of a request body: none. The server hands your code a stream. Without a limit the handler read 3 MiB. With a limit of 1 MiB it answered 413 after reading 2 MiB.
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node node_defaults.mjs
Four limits have values: 60 s for headers, 300 s for a whole request, 5 s of keep-alive and 16 KiB of headers. The socket timeout and the requests per connection are off, and the body has no setting at all, because the server hands your code a stream. With a count, the 1 MiB limit stopped after 2 MiB, since it counts whole chunks. The rest of this lesson takes the limits one at a time:
- A size limit refuses a body, header or URL above a number of bytes before it is read or parsed.
- A shape limit caps nesting depth, item counts and string lengths in structured input.
- A time limit ends a request or call that takes too long, so nothing is held for ever.
- A concurrency limit caps how many requests of one kind run at once and sheds the rest with a 503.
- A ReDoS defence is a rewrite of the pattern, a cap on the input, a linear-time engine or a deadline in an isolated worker.
- A decompression limit caps the bytes that a compressed input may produce, so that a small upload cannot become a huge one.
Checkpoint
Check: Limits are decisions
Flashcards · 4 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
What is resource exhaustion, and which CWE names its cause?
Requests use up something the service shares: CPU time, memory, connections, disk or a paid quota. A bug, a retry loop or one large customer causes it as easily as an attacker does. CWE-770 is allocation of resources without limits or throttling, and OWASP's A10:2025 says nothing in information technology should be limitless.
-
Why do limits matter most near capacity?
A server slows down fast there. In the single-queue model with 10 ms of service, 90 arrivals a second gave an average request time of 100 ms, ten times the service time, and 99 arrivals a second gave 1,000 ms, a hundred times.
-
Which limits does a plain Node.js server decide, and which does it leave to you?
It has values for headers (60 s), a whole request (300 s), keep-alive (5 s) and header size (16 KiB). The socket timeout and the requests per connection are off, and the body has no setting at all, because the server hands your code a stream; a 1 MiB count that adds whole chunks stopped after 2 MiB.
-
Name the six kinds of limit the lesson covers.
A size limit (refuse a body, header or URL above a number of bytes before reading it); a shape limit (nesting depth, item counts, string lengths); a time limit; a concurrency limit (cap simultaneous requests of one kind and shed the rest with a 503); a ReDoS defence (rewrite the pattern, cap the input, a linear-time engine or a deadline in an isolated worker); and a decompression limit.
Every input has a size, a shape and a time budget Pro: opens with a Pro, Premium or Ultimate pass
In this part:
- Timeouts and concurrency: a slow request holds something
- ReDoS: how one pattern turns 30 characters into half a billion tries
- Small inputs that cause a lot of work: compressed streams and nested loops
- Practise: cap the input, fix the pattern, bound the import
- Cap the input, fix the pattern, bound the import
- Interview questions
- What is ReDoS, and which kind of regular expression is vulnerable to it?
- After a release, the product search endpoint pins one CPU core for seconds on certain inputs, and the service falls behind. A profile shows the time inside a regular expression. How do you stop the incident, fix it properly, and make sure it cannot return?
- A download-all-orders export takes 40 seconds. When three customers press it together, every other endpoint times out. Redesign the export and describe the limits around it.
- You are adding a bulk-import endpoint that accepts gzip-compressed JSON with up to 5,000 rows. List the limits you set, where each is enforced, and what each refusal looks like.
- Key takeaways
1 diagram · 7 runnable examples (JavaScript, Python) · 1 exercise with sample tests · 3 interview answers · about 41 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Terms in this lesson
- catastrophic backtracking
- What a backtracking regular expression engine does on a pattern with several ways to match the same text: after a failure it tries every other way, and their number grows exponentially with the length of the input.
- decompression bomb
- A small compressed file that unpacks to a very large one, so that unpacking it fills memory or disk. A zip file's table of contents lists the sizes, so a program can refuse before it unpacks anything.
- load shedding
- Refusing some requests on purpose, usually with a 503 and a Retry-After header, when a service is near its limits, so that the requests it does accept still finish in time.
- ReDoS
- Regular expression denial of service: an input that makes a backtracking regular expression take exponentially longer as it grows, so that one request ties up a process for seconds or hours. CWE-1333 names the weakness.
- resource exhaustion
- A condition in which requests use up something the service shares, such as CPU time, memory, connections, disk or a paid quota, so that it slows down or stops for everyone. CWE-770 names the missing limits behind it.
References
- A10:2025 Mishandling of Exceptional Conditions (OWASP Foundation)
- API4:2023 Unrestricted Resource Consumption (OWASP Foundation)
- Denial of Service Cheat Sheet (OWASP Foundation)
- ASVS 5.0.0, V15 Secure Coding and Architecture (15.1.3 and 15.2.2) (OWASP Foundation)
- ASVS 5.0.0, V1 Encoding and Sanitization (1.3.12) (OWASP Foundation)
- ASVS 5.0.0, V5 File Handling (5.2.3 and 5.2.6) (OWASP Foundation)
- CWE-770: Allocation of Resources Without Limits or Throttling (The MITRE Corporation (CWE))
- CWE-1333: Inefficient Regular Expression Complexity (The MITRE Corporation (CWE))
- Node.js documentation: http, the server timeouts and maxHeaderSize (OpenJS Foundation)
- Node.js documentation: vm, the timeout option (OpenJS Foundation)
- Node.js documentation: AbortSignal.timeout() (OpenJS Foundation)
- nginx: ngx_http_core_module (client_max_body_size, client_body_timeout, large_client_header_buffers) (nginx)
- Flask configuration: MAX_CONTENT_LENGTH, MAX_FORM_MEMORY_SIZE and MAX_FORM_PARTS (Pallets)
- Django settings: DATA_UPLOAD_MAX_MEMORY_SIZE and DATA_UPLOAD_MAX_NUMBER_FIELDS (Django Software Foundation)
- Django exceptions: SuspiciousOperation answers with a 400 response (Django Software Foundation)
- Express body-parser: the limit option (Express (OpenJS Foundation))
- re: regular expression operations (Python 3.14) (Python Software Foundation)
- zipfile: work with ZIP archives (Python 3.14), decompression pitfalls (Python Software Foundation)
- zlib: compression compatible with gzip, Decompress.decompress and max_length (Python 3.14) (Python Software Foundation)
- Node.js documentation: zlib, the maxOutputLength option (OpenJS Foundation)
- An additional non-backtracking RegExp engine (V8) (V8 project)
- regexp package (Go): linear-time matching (The Go Authors)
- RFC 9110: HTTP Semantics (408, 413, 414, 503 and Retry-After) (IETF (RFC Editor))
- RFC 6585: Additional HTTP Status Codes (429 and 431) (IETF (RFC Editor))
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress