Memory Exhaustion Vulnerability in Envoy Zstd Decompressor
Summary
A vulnerability has been identified in Envoy's zstd decompressor implementation (ZstdDecompressorImpl). When zstd decompression is enabled, processing a specially crafted, highly compressed zstd payload can lead to massive memory allocation. An attacker can exploit this to cause severe memory exhaustion, potentially resulting in an Out-Of-Memory (OOM) kill and Denial of Service (DoS) for the Envoy proxy.
Details
The root cause lies in how inflation limits are enforced during decompression. Envoy correctly defines a MaxInflateRatio to prevent decompression bombs. However, the size check evaluation is incorrectly placed outside the inner execution loop.
In ZstdDecompressorImpl::decompress, the decompressor's outer loop iterates over the input buffer slices, making a check against MaxInflateRatio only after the process() call returns.
// zstd_decompressor_impl.cc
uint64_t limit = MaxInflateRatio * input_buffer.length();
for (const Buffer::RawSlice& input_slice : input_buffer.getRawSlices()) {
setInput(input_slice);
if (!process(output_buffer)) { return; } // <--- Runs to completion for the slice
if (... && (output_buffer.length() > limit)) { // <--- Limit check happens too late
return;
}
}
The process() method itself loops internally until the entire input slice is completely drained:
// zstd_decompressor_impl.cc
bool ZstdDecompressorImpl::process(Buffer::Instance& output_buffer) {
while (input_.pos < input_.size) {
ZSTD_decompressStream(dctx_.get(), &output_, &input_);
getOutput(output_buffer); // <--- Appends up to chunk_size bytes unconditionally
}
return true;
}
Because getOutput() unconditionally adds the decompressed chunks to the output buffer without enforcing the limit, the inner while loop completely bypasses the inflation ratio safeguards. A crafted 16KB payload can expand indefinitely (e.g., up to 500+ MB) within a single slice's processing step before the outer loop ever has a chance to check the bounds and abort.
By comparison, other decompressors in Envoy (like gzip and Brotli) correctly enforce memory and chunk limits within their inner process() loops.
Impact
Severity: High
Vulnerability Type: Denial of Service (DoS) / Resource Exhaustion (CWE-400)
Any attacker that can send requests to an Envoy endpoint utilizing the Zstd decompressor can send a maliciously crafted Zstd block (a "zip bomb" variant). This payload will reliably cause Envoy to spike in memory usage beyond safe limits, leading to process memory exhaustion and subsequent OOM termination. This results in a Denial of Service for all traffic passing through the affected Envoy instance.
Affected Components
- Envoy proxy Zstd compression extension (
envoy.compression.zstd.decompressor)
Workarounds
If an immediate patch cannot be applied, Envoy operators can mitigate this vulnerability by:
- Disabling the Zstd decompressor in the Envoy configuration.
- Temporarily falling back to other unimpacted decompression methods (such as Gzip or Brotli) which correctly enforce
MaxInflateRatio in their inner processing loops.
Recommended Fix
The ZstdDecompressorImpl implementation must be updated to enforce the MaxInflateRatio limit inside the inner while loop of the process() function, ensuring that decompression gracefully aborts as soon as the threshold is breached, rather than after the entire slice is processed. This brings the Zstd decompressor's behavior in line with Envoy's gzip and brotli implementations.
A patch to address this issue involves passing the limit explicitly into the process() function and checking the output buffer length after each chunk generation
Credit
Credits also go to Bruce Dang (bruce@calif.io)
Memory Exhaustion Vulnerability in Envoy Zstd Decompressor
Summary
A vulnerability has been identified in Envoy's zstd decompressor implementation (
ZstdDecompressorImpl). When zstd decompression is enabled, processing a specially crafted, highly compressed zstd payload can lead to massive memory allocation. An attacker can exploit this to cause severe memory exhaustion, potentially resulting in an Out-Of-Memory (OOM) kill and Denial of Service (DoS) for the Envoy proxy.Details
The root cause lies in how inflation limits are enforced during decompression. Envoy correctly defines a
MaxInflateRatioto prevent decompression bombs. However, the size check evaluation is incorrectly placed outside the inner execution loop.In
ZstdDecompressorImpl::decompress, the decompressor's outer loop iterates over the input buffer slices, making a check againstMaxInflateRatioonly after theprocess()call returns.The
process()method itself loops internally until the entire input slice is completely drained:Because
getOutput()unconditionally adds the decompressed chunks to the output buffer without enforcing thelimit, the innerwhileloop completely bypasses the inflation ratio safeguards. A crafted 16KB payload can expand indefinitely (e.g., up to 500+ MB) within a single slice's processing step before the outer loop ever has a chance to check the bounds and abort.By comparison, other decompressors in Envoy (like gzip and Brotli) correctly enforce memory and chunk limits within their inner
process()loops.Impact
Severity: High
Vulnerability Type: Denial of Service (DoS) / Resource Exhaustion (CWE-400)
Any attacker that can send requests to an Envoy endpoint utilizing the Zstd decompressor can send a maliciously crafted Zstd block (a "zip bomb" variant). This payload will reliably cause Envoy to spike in memory usage beyond safe limits, leading to process memory exhaustion and subsequent OOM termination. This results in a Denial of Service for all traffic passing through the affected Envoy instance.
Affected Components
envoy.compression.zstd.decompressor)Workarounds
If an immediate patch cannot be applied, Envoy operators can mitigate this vulnerability by:
MaxInflateRatioin their inner processing loops.Recommended Fix
The
ZstdDecompressorImplimplementation must be updated to enforce theMaxInflateRatiolimit inside the innerwhileloop of theprocess()function, ensuring that decompression gracefully aborts as soon as the threshold is breached, rather than after the entire slice is processed. This brings the Zstd decompressor's behavior in line with Envoy's gzip and brotli implementations.A patch to address this issue involves passing the
limitexplicitly into theprocess()function and checking the output buffer length after each chunk generationCredit
Credits also go to Bruce Dang (bruce@calif.io)