A service may reject a 100 MB upload and still accept a much smaller compressed file that expands far beyond the memory or storage the service can afford. The upload limit measured the bytes crossing one boundary. The expensive work happens after that boundary, when the application decompresses, parses, indexes, scans, or stores the expanded data.
This is the practical problem behind decompression bombs: compact input can cause disproportionate resource use when software expands it without enforcing a budget on the result. The consequence is usually availability loss rather than unauthorized access. Workers can run out of memory, temporary storage can fill, CPU time can be consumed, and a queue of expensive jobs can delay ordinary requests.
The defensive mental model is simple: limit the resource-consuming representation, not only the representation you received. This article shows how to turn that idea into enforceable limits and where those limits need to live.
Compressed size and processing cost are different quantities
Suppose an import endpoint accepts archives up to 20 MB. A request passes that check, so the service starts extracting it.
received bytes: 8 MB
allowed upload size: 20 MB
8 MB passes the upload check
|
v
archive expands during processing
|
v
hundreds of MB or more of outputThe exact expansion depends on the compression format and data, so the numbers above are illustrative rather than a property of compression in general. The security point is that an input-size check does not bound decompressed output.
The same distinction appears outside archive extraction. An HTTP request body may use content encoding. A document may contain compressed embedded objects. A backup importer may unpack several layers before parsing records. In each case, the application needs to ask what resource grows after decoding begins.
The threat model here is an untrusted or insufficiently trusted sender who can supply compressed data to a service that expands it. The control aims to reduce resource-exhaustion risk caused by excessive expansion or processing work. It does not make the decompressed content trustworthy, fix vulnerabilities in the decompressor, stop path traversal during extraction, or replace broader request and workload rate limits.
Give decompression an explicit budget
A useful design starts with a budget expressed in resources the service actually needs to protect.
For a simple endpoint that accepts one compressed payload, the most important limit may be total decompressed bytes. Instead of asking the decompressor to produce arbitrary output and checking the size afterward, the application stops reading once the allowed output budget is exceeded.
Conceptually:
maximum expanded bytes = application policy
expanded bytes so far = 0
for each output chunk:
expanded bytes so far += chunk size
if expanded bytes so far > maximum expanded bytes:
stop processing and reject the inputThe check must happen while data is being produced. If the program first expands everything into memory and only then compares the length, the resource exhaustion has already happened.
This pattern is deliberately language-neutral. Production code should use the streaming and bounded-reader facilities provided by its runtime or decompression library, and it must handle the library’s error and close semantics correctly.
Archives need more than one limit
An archive is a collection, so total expanded bytes are only one dimension of work.
Imagine an archive whose total extracted data is within the byte budget but which contains an extremely large number of tiny entries. The service may still spend excessive time creating files, allocating metadata, scanning each entry, or inserting database records.
For archive processing, a practical policy often needs several independent bounds:
- maximum compressed input size;
- maximum total uncompressed bytes processed;
- maximum uncompressed size for one entry;
- maximum number of entries;
- maximum nesting depth when the application deliberately supports nested containers;
- execution or job limits appropriate to the processing environment.
These limits protect different resources. A total-byte limit constrains aggregate output. A per-entry limit stops one member from consuming the whole budget unexpectedly. An entry-count limit constrains per-object overhead. A nesting limit prevents a workflow from repeatedly discovering more compressed containers than it intended to process.
There is no universal number for these limits. A profile-photo service, a source-code scanner, and a backup restoration tool have different legitimate workloads. Choose limits from the feature’s expected inputs and available capacity, then test realistic files near those boundaries.
Do not trust archive metadata as the enforcement point
Many formats can describe properties such as an entry’s expected uncompressed size. That metadata can be useful for early rejection and capacity planning, but security enforcement should not depend on a sender-controlled declaration being accurate.
A robust design treats declared sizes as hints and enforces the budget against bytes actually produced by decompression.
declared size -> optional early check
actual output -> authoritative runtime limitThis distinction matters because the parser is crossing a trust boundary. The service is deciding how much memory, disk, CPU, and downstream work to spend based on data supplied by someone else. The final accounting should come from resources the service observes, not only values the input claims.
The same principle applies to entry counts and nesting. If processing discovers more entries or layers than policy permits, stop even if an earlier header suggested that the work would be smaller.
Count across the whole operation
A common implementation mistake is to reset the limit for every archive member.
Suppose policy allows 100 MB of extracted data. If each of 1,000 entries independently receives a 100 MB allowance, the policy is not a 100 MB archive limit. It is potentially a much larger workload.
Keep aggregate accounting at the scope where the security decision applies:
job budget: 100 MB total
entry A consumes 30 MB -> 70 MB remains
entry B consumes 50 MB -> 20 MB remains
entry C tries 25 MB -> stopPer-entry limits can still exist, but they should complement the total budget rather than replace it.
The same reasoning applies when processing is split across functions or services. If one component extracts an archive and another generates previews, each component may enforce its own local safety limits, while the overall job also needs a bounded resource envelope. Otherwise moving work between components can accidentally remove the effective cap.
Nested compression changes where the budget must follow
Some applications intentionally accept containers that can contain other compressed containers. A security scanner, for example, may need to inspect files inside an archive and then inspect an archive found inside it.
If each nested layer starts with a fresh unlimited budget, the outer check provides little protection. The processing budget must follow the job through the layers the application chooses to inspect.
A useful model is:
job
├── remaining expanded-byte budget
├── remaining entry budget
└── remaining nesting depthEach layer consumes from those shared limits. When a limit is exhausted, processing stops according to a defined failure policy.
Not every application should support nested archives. If the feature does not require them, rejecting nested containers is simpler than safely handling arbitrary recursion. Supporting less input is often the strongest resource-control decision available.
Streaming helps only when the stream is bounded
Streaming decompression is useful because the application does not need to hold the entire expanded payload in memory. But streaming by itself is not a security limit.
An unbounded stream can keep producing data until disk fills, a downstream service becomes overloaded, or the worker spends too long processing it. The stream needs a counter or bounded sink that can stop the operation when policy is exceeded.
This also means that changing the destination from memory to temporary files does not solve the underlying problem. It changes which resource is at risk. Disk-backed extraction may be appropriate for large legitimate workloads, but then free space, quotas, cleanup, and concurrent jobs become part of the operational design.
Compression ratios are useful signals, not sufficient budgets
It can be tempting to reject any input whose expansion ratio exceeds a fixed threshold:
expansion ratio = uncompressed bytes / compressed bytesA ratio can help identify unusual inputs, but it is a poor sole control. Highly compressible legitimate data can have a large ratio, while a large compressed input with a moderate ratio can still exceed the service’s absolute capacity.
Absolute resource limits answer the question the service actually cares about: how much output or work will this operation be allowed to consume? A ratio threshold can be an additional policy or monitoring signal when it fits the workload, but it should not substitute for a hard output budget.
Decide how failure behaves before a limit is hit
Resource limits are most useful when exceeding them has a predictable result.
For synchronous processing, the application should stop decompression, discard partial results that should not survive, and return an error appropriate to its interface. For asynchronous jobs, the worker should mark the job as rejected or failed rather than retrying the same deterministic oversized input indefinitely.
Cleanup matters. If extraction writes temporary files, failure handling should remove or expire partial data. If downstream records have already been created, the workflow needs transactional or compensating behavior appropriate to the application so a rejected archive does not leave a misleading half-imported state.
Be careful with error detail exposed to untrusted clients. A message such as “expanded content exceeds the allowed processing limit” is usually enough. Internal logs can record which budget was reached and how much work had been observed, subject to the application’s logging and data-minimization rules.
Concurrency turns a per-job limit into a capacity question
A 200 MB per-job memory allowance may be reasonable for one worker and unreasonable if 50 such jobs can run concurrently. Per-input limits constrain individual operations; they do not automatically protect shared capacity.
This is where defense in depth becomes necessary. Bound each decompression job, then also control how many expensive jobs can execute at once. Depending on the architecture, that may mean worker concurrency limits, queue admission controls, memory or storage quotas, request throttling, or isolation provided by the execution environment.
The relationship is straightforward:
bounded job cost
+
bounded concurrency
=
more predictable maximum pressureIt is still an estimate rather than a mathematical guarantee if other workloads share the same resources. Measure actual memory, CPU, temporary storage, and processing time under representative inputs before setting production capacity assumptions.
Test the boundary, not only normal files
A decompression limit is easy to implement incorrectly because ordinary test fixtures never reach it.
Tests should cover an input comfortably below the limit, one that reaches the intended boundary, and one that exceeds it while output is being streamed. For archives, also test too many entries, one oversized entry, aggregate size spread across many entries, and excessive nesting if nesting is supported.
Verify more than the returned error. Confirm that processing stops promptly, partial temporary data is cleaned up as designed, the job is not automatically retried forever, and another normal request can still make progress. Those observations test the security property rather than merely the validation branch.
Keep the budget next to the decompression boundary
An upload-size limit protects network and initial storage costs. It cannot tell you how expensive the accepted bytes will become after decompression.
When a service expands untrusted data, define the maximum output and work the feature is willing to consume, enforce those limits while processing, and carry aggregate budgets across entries and supported nesting. Add concurrency controls when many individually bounded jobs can still overwhelm shared capacity.
The next practical step is to find every place your application decompresses data and write down what currently bounds expanded bytes, object count, nesting, execution time, and concurrent work. Any blank answer marks a resource boundary that is relying on the input to behave.