Skip to content

Zstd Decompressor: Ratio Check at Wrong Loop Depth lead to memory explosion

High
nezdolik published GHSA-m3p9-47wh-88wg Jun 23, 2026

Package

gomod github.com/envoyproxy/envoy (Go)

Affected versions

>=1.23,<1.39

Patched versions

1.35.13 1.36.9 1.37.5 1.38.3

Description

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:

  1. Disabling the Zstd decompressor in the Envoy configuration.
  2. 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)

Severity

High

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
Low
Privileges required
None
User interaction
None
Scope
Unchanged
Confidentiality
None
Integrity
None
Availability
High

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

CVE ID

CVE-2026-48044

Weaknesses

No CWEs

Credits