An upload limit can look like a complete resource limit until the application accepts compressed input. A small archive may expand into far more data than its uploaded size suggests, contain an excessive number of entries, or require enough decompression work to tie up workers. If the service trusts the compressed size, an attacker may be able to exhaust disk, memory, CPU time, or processing capacity without sending a large request.
The defensive idea is simple: limit the resources produced and consumed during decompression, not only the bytes received over the network.
This article develops that idea into a practical design. You will learn which resource boundaries matter, where to enforce them, why metadata checks alone are insufficient, how nested archives change the model, and how to test the control without relying on dangerous examples.
Compressed size and expanded cost are different quantities
Suppose an application accepts archive uploads up to 20 MB. It checks the HTTP body size and then extracts an accepted archive into a temporary directory.
The check proves one useful fact:
uploaded bytes <= 20 MBIt does not prove any of these:
expanded bytes <= 20 MB
number of files is small
memory use is bounded
CPU work is bounded
temporary storage is boundedCompression works precisely because some inputs can be represented using fewer bytes than their expanded form. The ratio between compressed and expanded size therefore cannot be treated as a fixed security constant unless the format, encoder, and accepted content impose a bound you have actually established.
This is the core mental model: the archive is a description of work and output, not merely a blob of the size you received.
The threat considered here is an untrusted sender who can submit compressed content to a service that decompresses it. The control aims to reduce availability risk from excessive expansion or processing. It does not by itself stop malicious file contents, path traversal during extraction, parser vulnerabilities, malware, or authorization mistakes. Those require separate controls.
Put limits around the resources you actually need to protect
A decompression boundary should correspond to resources whose exhaustion would harm the service.
For a typical archive-processing workflow, useful limits may include:
- total uncompressed bytes produced;
- uncompressed bytes for any single entry;
- number of archive entries processed;
- memory used by buffering or downstream parsing;
- temporary disk space consumed;
- processing time or other work budget;
- nesting depth if archives inside archives are intentionally supported.
These limits solve different failure modes. A total-byte limit does not stop an archive containing millions of empty or tiny entries from creating excessive filesystem or metadata work. A file-count limit does not stop one permitted entry from expanding beyond available storage. A timeout can stop work eventually, but it may still allow many concurrent requests to consume substantial resources before they time out.
Do not choose numbers merely because they sound conservative. Start from the product requirement and deployment capacity. If legitimate users need to upload 500 MB of expanded data, a 50 MB expansion limit is unusable. If a worker has 1 GB of memory, accepting several concurrent jobs that each buffer hundreds of megabytes can still be unsafe even when each job stays below its individual limit.
The right question is: what is the largest legitimate job this service must support, and how many such jobs can the system safely process at once?
Enforce the byte limit while data is being produced
Archive metadata often reports an entry’s expected uncompressed size. That information is useful for rejecting obviously oversized input early, but untrusted metadata should not be the only enforcement point.
A safer pattern counts actual decompressed bytes as they are emitted:
expanded_total = 0
for each permitted archive entry:
entry_total = 0
while decompressed chunk is available:
entry_total += length(chunk)
expanded_total += length(chunk)
if entry_total > per_entry_limit:
stop processing
if expanded_total > archive_limit:
stop processing
write or process chunkThis pseudocode demonstrates the security property rather than a specific library API. Production code must also handle library errors, partial output, cleanup, path safety, concurrency, and format-specific behavior.
The important ordering is that the limit is checked as output is produced. The application should not first expand the complete entry into memory and then ask whether the result was too large. At that point the resource you wanted to protect may already be exhausted.
Streaming also makes the control easier to reason about. A bounded chunk can be counted, checked, and passed onward without requiring the entire expanded object to exist in memory at once. This reduces memory pressure, although it does not remove the need to bound disk, CPU, entry count, or concurrent work.
Treat advertised sizes as hints, not proof
Many archive formats carry metadata such as compressed size and uncompressed size. It is tempting to calculate an expansion ratio before extraction:
reported_uncompressed_size / compressed_sizeThat can be a useful early rejection signal, but it should not replace an absolute output limit.
First, the metadata itself comes from untrusted input. Whether and how a particular library validates it is format- and implementation-dependent. Second, a ratio answers a different question from capacity. A 1 MB archive expanding to 100 MB and a 100 MB archive expanding to 10 GB may have the same ratio while presenting very different operational consequences. Third, some legitimate highly compressible content can naturally have a large ratio.
For defensive decisions, absolute resource ceilings are easier to connect to system capacity:
actual expanded bytes <= configured maximum
actual entries processed <= configured maximumYou may combine these with metadata-based prechecks to reject work sooner, but keep runtime accounting as the enforcement mechanism.
Count entries as well as bytes
An archive can create work even when its total file contents are small.
Each entry may require parsing metadata, validating a name, allocating objects, creating filesystem state, updating indexes, or invoking downstream processing. A very large entry count can therefore consume CPU, memory, file descriptors, or filesystem metadata without exceeding a generous expanded-byte limit.
Add an entry budget before performing expensive work:
entries_seen += 1
if entries_seen > entry_limit:
stop processingDecide what counts as an entry according to the actual archive format and application behavior. Files, directories, links, or other special entry types may have different semantics. If the application does not need a type, rejecting it is usually simpler than trying to support it safely.
The entry limit should also match the downstream workflow. If extraction is followed by scanning every file, the scanner’s practical capacity matters. Allowing 100,000 entries because the extraction library can enumerate them is not useful if the next stage can safely process only 5,000 within the job deadline.
Decide whether nested archives are data or instructions
Nested compression changes the trust boundary.
Suppose an uploaded archive contains another archive. If your application extracts only the outer archive and treats the inner file as ordinary data, the inner archive does not automatically consume another decompression budget. If the application recursively opens archives it finds, however, each layer can create additional work and output.
Do not enable recursive extraction merely because a library can do it. Ask whether the product actually requires it.
If nesting is required, keep one budget across the whole logical job rather than resetting limits for every child archive:
job budget
|
+-- outer archive output
| |
| +-- nested archive output
| |
| +-- deeper output
|
+-- all consume the same total budgetAlso set a maximum nesting depth. A byte budget bounds output volume, while a depth limit bounds recursive structure and helps keep control flow predictable. Neither replaces the other.
If nesting is not required, treat embedded archives as files and do not recursively expand them. The simplest parser path is often the easiest one to secure and operate.
Bound concurrency, not only individual requests
A perfect per-archive limit can still leave the service vulnerable to aggregate exhaustion.
Imagine a worker can safely handle one job that expands to 500 MB of temporary data. Ten such jobs arriving together may require 5 GB. If the host has less free capacity, every request can obey its own limit while the system as a whole fails.
This is why decompression limits need an operational counterpart: admission control.
A service can restrict the number of active decompression jobs, reserve capacity before accepting expensive work, queue jobs with a bounded queue, or isolate decompression workers with resource limits appropriate to the platform. The exact mechanism depends on the architecture. The security principle is that the sum of permitted work must fit inside a controlled system budget.
This also explains why request rate limiting alone is not enough. Ten small requests may have radically different decompression costs. Rate limiting can reduce request floods, while decompression budgets constrain the cost of requests that are admitted. The controls complement each other.
Fail closed without leaving expensive debris
When a limit is exceeded, stop processing the archive and make the partial state predictable.
If extraction writes directly into a permanent destination, a rejected archive may leave a half-created dataset behind. That can confuse later processing and consume storage long after the request has failed.
A safer workflow uses a temporary, isolated destination for the job:
receive archive
|
v
extract into temporary job area
|
limits pass?
/ \
no yes
| |
cleanup publish or moveThe final publish step should happen only after the archive has passed the checks required by the application. Cleanup itself should be bounded and reliable; creating an enormous amount of temporary filesystem state and then depending on expensive synchronous cleanup can create another resource problem.
Record the rejection reason at a useful level, such as total expanded-byte limit or entry-count limit, without logging untrusted file contents. Metrics for rejected jobs, bytes produced before rejection, processing duration, and active worker count can help operators detect abuse or simply discover that legitimate workloads no longer fit the configured assumptions.
Keep path safety as a separate control
Resource limits do not make archive extraction generally safe.
An archive entry name may attempt to resolve outside the intended extraction directory. Some archive formats can also represent links or other special filesystem objects. Those are different security problems from decompression exhaustion.
Keep the reasoning separate:
resource question: how much work or output may this archive create?
path question: where may an accepted entry be written?
type question: which entry types does the application support?
content question: what may downstream parsers safely consume?Combining all of these under a vague label such as “validate the archive” makes reviews harder because a passing size check can be mistaken for broader safety.
For the resource control described in this article, the guarantee is narrow: under the configured assumptions, one admitted job cannot produce more counted output or entries than its limits allow. Complementary path, type, parser, authorization, and malware controls still need their own designs where relevant.
Test boundaries with generated benign data
You do not need a weaponized archive to verify the defense.
Build tests that generate benign compressed fixtures around each configured boundary. For example, if a test environment uses a 10 MB expanded-byte limit, create fixtures that expand to just below the limit, exactly the limit if the policy permits it, and just above it. Do the same for entry count and nesting depth.
Verify both the decision and the side effects:
within limit -> accepted as expected
above limit -> rejected before exceeding allowed output
rejected job -> partial data is not published
next job -> worker remains healthyAdd concurrency tests at the service level. Several individually valid jobs should not be able to exceed the worker pool’s intended aggregate capacity. Also test truncated or malformed archives to confirm that parser errors follow the same cleanup path as explicit limit failures.
Finally, observe memory, temporary storage, CPU time, and job duration during these tests. A byte counter can be logically correct while a library buffers unexpectedly large structures internally. Measurements help verify that the implementation matches the mental model.
Choose the simplest control that matches the workload
Not every application needs a complex archive-processing subsystem.
If users only need to upload one document, accepting that document directly may remove the decompression problem entirely. If archives are necessary but nested archives are not, disable recursive processing. If extraction happens rarely in a trusted administrative workflow, conservative per-job limits and low concurrency may be enough. A public high-volume ingestion service usually needs tighter admission control, isolation, monitoring, and capacity testing.
The goal is not to reject compression. Compression is useful. The goal is to stop treating compressed input size as evidence of bounded processing cost.
Conclusion
An archive upload is a request to perform work. The bytes on the wire describe only part of that work.
Start with the resources the service must protect: expanded bytes, entries, memory, temporary storage, processing time, nesting, and aggregate concurrency where they matter. Enforce output limits while decompression is happening, use archive metadata only as an early signal, and keep one budget across recursive processing if recursion is genuinely required. Clean up rejected jobs without publishing partial results, and test the boundaries with benign generated fixtures.
The practical question for every decompression feature is: what can this small input cause the service to allocate, write, or process before the service says no? Put the security boundary around that cost.