← Back to Feed CACHED · 2026-09-12 08:20:55 · CACHE_KEY CVE-2026-87776
CVE-2026-87776 · CWE-401 · Disclosed 2026-09-11

compression is a Node.js and Express compression middleware.

ASSESSED — NOISGATE V0.5
Vendor
Reassessed
Verdict:
Do you agree?
01 · The Real Story

Like a restaurant that never clears dirty dishes when guests leave early, until the kitchen runs out of plates

CVE-2026-87776 affects the compression npm middleware (all versions prior to 1.8.2), the de-facto standard HTTP response compression layer for Express.js applications with over 8,600 direct dependents on npm. The flaw is a classic CWE-401 resource leak: when a client disconnects while a compressed response is still being written, the underlying zlib stream is never .destroy()'d. Each aborted request permanently leaks native (C-level) memory outside the V8 heap. An unauthenticated remote attacker can open thousands of connections to a compressed endpoint, abort them mid-stream, and exhaust the process's RSS until the OS OOM-killer terminates it.

The vendor scores this HIGH at CVSS 7.5, which is technically correct for an unauthenticated, low-complexity, no-interaction remote DoS. However, the real-world severity is narrower than the score implies. The impact ceiling is availability-only — no code execution, no data exfiltration, no integrity violation. Modern Node.js deployments almost universally run behind process managers (PM2, systemd, container orchestrators) that auto-restart crashed workers within seconds, and horizontal scaling behind load balancers further absorbs single-process failures. The leak is also rate-dependent: crashing a production process requires sustained concurrent aborted connections, which is visible in access logs and rate-limit layers. We downgrade to MEDIUM 6.2.

"Unauthenticated memory-leak DoS in Express compression middleware, capped at availability impact."
02 · The Attack Path

4 steps from start to impact.

STEP 01

Identify a compression-enabled endpoint

The attacker sends a request with Accept-Encoding: gzip, deflate, br to any route on the target Express app. If the response comes back with Content-Encoding: gzip (or deflate/br), the compression middleware is active. No authentication is required. Most Express apps using compression() compress all responses above a 1 KB threshold by default.
Conditions required:
  • Target runs Express (or Connect-compatible) app with compression middleware < 1.8.2
  • Endpoint is network-reachable
Where this breaks in practice:
  • Endpoint may sit behind a CDN or reverse proxy (nginx, Cloudflare) that handles compression itself, making the Express-level middleware unused
Detection/coverage: Standard HTTP request — no scanner signature fires at this stage.
STEP 02

Open many connections, begin receiving compressed responses

The attacker opens hundreds to thousands of concurrent HTTP connections to the identified endpoint. Each request triggers the compression middleware to create a new zlib Transform stream. The attacker must let the server begin writing compressed data so the zlib stream is allocated in native memory.
Conditions required:
  • Attacker can open concurrent TCP connections to the target
  • Responses are large enough to exceed the compression threshold (default 1024 bytes)
Where this breaks in practice:
  • Rate limiters, connection-count limits, or WAF rules may cap concurrent connections from a single IP
  • Cloud WAFs (Cloudflare, AWS WAF) enforce per-IP connection budgets by default
Detection/coverage: Connection-rate anomaly detection; high concurrent connection count from single source IP.
STEP 03

Abort connections mid-stream to trigger zlib leak

Before each response finishes writing, the attacker sends a TCP RST or simply closes the socket. The Express res object emits a close event, but the compression middleware (pre-1.8.2) does not call .destroy() on the zlib stream in that handler. The native zlib memory (~256 KB per stream at default memLevel=8) is permanently leaked. The attacker repeats this cycle.
Conditions required:
  • Compression middleware version < 1.8.2
  • Responses are large enough that the server is still writing when the client disconnects
Where this breaks in practice:
  • Requires sustained, repeated connections — a single request leaks only ~256 KB
  • Reaching OOM on a 512 MB container requires ~2,000 leaked streams; on a 4 GB server, ~16,000
Detection/coverage: Node.js process RSS growth without GC recovery is observable via APM tools (Datadog, New Relic, Dynatrace). Heap vs RSS divergence is a classic native-memory-leak signal.
STEP 04

Process OOM and service disruption

Once native memory exhausts the process's allocation limit or the container's cgroup memory cap, the OS OOM-killer terminates the Node.js process. If no process manager is configured, the service stays down. Even with auto-restart, the attacker can repeat the cycle to cause repeated restarts (crash loop).
Conditions required:
  • No process manager or orchestrator auto-restart, OR attacker sustains the attack through restart cycles
Where this breaks in practice:
  • PM2, systemd, and Kubernetes restart crashed processes in < 5 seconds
  • Kubernetes will back off restarts (CrashLoopBackOff) but the pod remains allocated
  • Horizontal pod autoscaling may spin up new replicas faster than the attacker can crash them
Detection/coverage: OOM-kill events are logged by the kernel (dmesg) and container runtime. Kubernetes emits OOMKilled pod status.
03 · Intelligence Metadata

The supporting signals.

In-the-Wild ExploitationNo known active exploitation. Not listed in CISA KEV. Disclosed 2026-09-11, so exploit activity may emerge.
Proof-of-ConceptNo public PoC repo identified yet. The attack is trivially reproducible: curl --compressed <url> & then kill the process mid-transfer. Finder credited as KKamJi98.
EPSS ScoreNot yet scored — CVE published 2026-09-11, EPSS model has not ingested it. Expected to land in the 5th–20th percentile range (DoS-only, no code exec).
KEV StatusNot listed. No CISA KEV entry as of 2026-09-12.
CVSS VectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H — network-reachable, no auth, no interaction, availability-only impact. Scope unchanged.
Affected Versionscompression < 1.8.2 (all prior releases including 1.8.1, 1.8.0, 1.7.x, and earlier).
Fixed Versioncompression 1.8.2. No distro backports expected — this is an npm package, not an OS-level dependency.
Exposure / Install Base~8,637 direct npm dependents. Weekly downloads estimated in the millions. Used by Express, Koa adapters, NestJS, and countless internal APIs. However, many production deployments offload compression to nginx/Cloudflare, making the Express middleware a no-op.
Disclosure Date2026-09-11 via GitHub Security Advisory GHSA-vc2v-76pw-4v95.
CreditsFound by KKamJi98, fixed by UlisesGascon, reviewed by Phillip9587, analyzed by bjohansebas.
04 · The Call

noisgate verdict.

Final Verdict
DOWNGRADED to MEDIUM (6.2/10)

The single most decisive factor is the availability-only impact ceiling: even a fully successful exploit chain produces only a process crash with no path to code execution, data exfiltration, or lateral movement, and modern orchestrators auto-recover in seconds. The vendor HIGH is technically valid by CVSS math but overstates operational risk for teams running standard container platforms.

HIGH Vulnerability mechanics and affected version range
HIGH Impact ceiling (DoS only, no RCE/data breach)
MEDIUM Real-world exposure estimate (many apps offload compression upstream)

Why this verdict

  • Unauthenticated remote, low complexity — the CVSS attack-vector and complexity ratings are accurate; any network client can trigger this without credentials or user interaction. This keeps the score from falling below MEDIUM.
  • Availability-only impact — no confidentiality or integrity loss. The worst outcome is a process crash. This is a hard ceiling on severity that the vendor CVSS correctly models (C:N/I:N/A:H) but that operational risk assessment should weight more heavily than raw score math.
  • Process-manager and orchestrator recovery — PM2, systemd, and Kubernetes restart crashed Node.js processes in 1–5 seconds. Sustained exploitation requires the attacker to maintain high connection rates through restart cycles, raising the practical bar.
  • Upstream compression offload — a material fraction of Express deployments offload gzip/brotli to nginx, Cloudflare, or a CDN, rendering the Express-level compression middleware a no-op and making the vulnerability unexploitable on those hosts.
  • Role multiplier: The compression middleware runs in Node.js application-tier servers. It is NOT canonically a high-value-role component (not an IdP, DC, hypervisor, backup server, or kernel agent). In the rare case it runs in an API gateway role, the blast radius is still capped at DoS of that gateway process — no domain takeover or fleet compromise is reachable. The floor rule for HIGH (fleet/domain/supply-chain impact) does not apply.

Why not higher?

Upgrading to HIGH would require either a path beyond denial-of-service (RCE, data exfil, lateral movement) or the affected component canonically occupying a high-value fleet role where DoS equals safety or operational catastrophe (e.g., OT/SCADA). The compression middleware is a general-purpose HTTP utility; crashing it does not cascade to identity, backup, or infrastructure-control planes.

Why not lower?

Dropping to LOW would understate the risk of a trivially exploitable, unauthenticated, zero-interaction remote DoS against a library with millions of weekly downloads. The attack requires no special tooling, the leak is deterministic, and not all deployments have robust auto-restart or upstream compression offload. MEDIUM correctly reflects the real operational risk.

05 · Compensating Control

What to do — in priority order.

  1. Offload compression to the reverse proxy or CDN — Configure nginx (gzip on;), Cloudflare, or your cloud LB to handle response compression. Then remove or disable app.use(compression()) from your Express app. This eliminates the vulnerable code path entirely. As a MEDIUM-severity finding, no mitigation SLA applies — go straight to the 365-day remediation window, but this control is low-effort and worth deploying sooner.
  2. Rate-limit concurrent connections per source IP — Apply per-IP connection limits at your load balancer or WAF (e.g., nginx limit_conn at 50–100 per IP). This throttles the leak rate, making OOM impractical within auto-restart windows.
  3. Set container memory limits and ensure restart policies — Ensure all Node.js containers have explicit memory cgroup limits and restartPolicy: Always (Kubernetes) or equivalent. This caps blast radius to a single pod restart rather than node-level OOM.
  4. Monitor RSS vs heap divergence — Alert on Node.js process RSS exceeding V8 heap used by more than 2x in your APM tool. This is the canonical signal for native memory leaks and will catch active exploitation.
What doesn't work
  • Node.js --max-old-space-size flag — this only limits the V8 JavaScript heap, not native C-level allocations. The zlib leak occurs in native memory outside the V8 heap, so this flag will not prevent OOM.
  • WAF payload inspection rules — the attack uses completely normal HTTP requests with standard Accept-Encoding headers. There is no malicious payload to signature-match.
06 · Verification

Crowdsourced verification payload.

Run this on each target host or in your CI pipeline against your package-lock.json / node_modules. No special privileges required. Example: bash check_compression_cve.sh /app where /app is your project root.

noisgate-verify.sh
BASHREAD-ONLYSAFE
#!/usr/bin/env bash
# CVE-2026-87776 checker for compression npm package
# Usage: bash check_compression_cve.sh <project_root>
# Exit codes: 0=PATCHED, 1=VULNERABLE, 2=UNKNOWN

set -euo pipefail

PROJECT_ROOT="${1:-.}"
FIXED_MAJOR=1
FIXED_MINOR=8
FIXED_PATCH=2

# Try package-lock.json first
if [ -f "$PROJECT_ROOT/package-lock.json" ]; then
  VERSION=$(python3 -c "
import json, sys
with open('$PROJECT_ROOT/package-lock.json') as f:
    lock = json.load(f)
# lockfile v2/v3
if 'packages' in lock:
    for key, val in lock['packages'].items():
        if key.endswith('/compression') or key == 'node_modules/compression':
            print(val.get('version', '')); sys.exit(0)
# lockfile v1
if 'dependencies' in lock and 'compression' in lock['dependencies']:
    print(lock['dependencies']['compression'].get('version', '')); sys.exit(0)
print('')
" 2>/dev/null)
elif [ -f "$PROJECT_ROOT/node_modules/compression/package.json" ]; then
  VERSION=$(python3 -c "
import json
with open('$PROJECT_ROOT/node_modules/compression/package.json') as f:
    print(json.load(f).get('version', ''))
" 2>/dev/null)
else
  echo "UNKNOWN - compression package not found in $PROJECT_ROOT"
  exit 2
fi

if [ -z "$VERSION" ]; then
  echo "UNKNOWN - could not determine compression version"
  exit 2
fi

IFS='.' read -r MAJ MIN PAT <<< "$VERSION"
MAJ=${MAJ:-0}; MIN=${MIN:-0}; PAT=${PAT:-0}

if [ "$MAJ" -gt "$FIXED_MAJOR" ] 2>/dev/null || \
   { [ "$MAJ" -eq "$FIXED_MAJOR" ] && [ "$MIN" -gt "$FIXED_MINOR" ]; } 2>/dev/null || \
   { [ "$MAJ" -eq "$FIXED_MAJOR" ] && [ "$MIN" -eq "$FIXED_MINOR" ] && [ "$PAT" -ge "$FIXED_PATCH" ]; } 2>/dev/null; then
  echo "PATCHED - compression $VERSION >= 1.8.2"
  exit 0
else
  echo "VULNERABLE - compression $VERSION < 1.8.2 (CVE-2026-87776)"
  exit 1
fi
07 · Bottom Line

If you remember one thing.

TL;DR
This is a real but bounded DoS vulnerability. At MEDIUM severity under the noisgate framework, there is no mitigation SLA — go straight to the 365-day noisgate remediation SLA and schedule npm update compression to pull in version 1.8.2 during your next regular dependency refresh cycle. If your Express apps handle compression at the application layer (check for app.use(compression()) in your codebase) and face the public internet without an upstream compression-capable proxy, move the update forward into your next sprint. If your nginx, Cloudflare, or cloud LB already handles gzip/brotli, the Express middleware is likely a no-op and your exposure is near zero — still update within the year but deprioritize versus any HIGH or CRITICAL items in your queue. No KEV listing and no active exploitation mean there is no emergency override.

Sources

  1. GHSA-vc2v-76pw-4v95 — GitHub Security Advisory
  2. CVE-2026-87776 — OpenCVE
  3. CVE-2026-87776 — THREATINT
  4. expressjs/compression — GitHub Repository
  5. Express.js Security Updates
  6. compression — npm Registry
  7. Express.js July 2026 Security Releases
Peer Review

What defenders are saying.

Submit a review attribution: handle + country only
0 flags selected · stored anonymously
Validation Results

Crowdsourced verification outputs.

Results submitted by users who ran the verification payload against their environment.