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.
4 steps from start to impact.
Identify a compression-enabled endpoint
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.- Target runs Express (or Connect-compatible) app with
compressionmiddleware < 1.8.2 - Endpoint is network-reachable
- Endpoint may sit behind a CDN or reverse proxy (nginx, Cloudflare) that handles compression itself, making the Express-level middleware unused
Open many connections, begin receiving compressed responses
- Attacker can open concurrent TCP connections to the target
- Responses are large enough to exceed the compression threshold (default 1024 bytes)
- 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
Abort connections mid-stream to trigger zlib leak
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.- Compression middleware version < 1.8.2
- Responses are large enough that the server is still writing when the client disconnects
- 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
Process OOM and service disruption
- No process manager or orchestrator auto-restart, OR attacker sustains the attack through restart cycles
- 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
dmesg) and container runtime. Kubernetes emits OOMKilled pod status.The supporting signals.
| In-the-Wild Exploitation | No known active exploitation. Not listed in CISA KEV. Disclosed 2026-09-11, so exploit activity may emerge. |
|---|---|
| Proof-of-Concept | No public PoC repo identified yet. The attack is trivially reproducible: curl --compressed <url> & then kill the process mid-transfer. Finder credited as KKamJi98. |
| EPSS Score | Not 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 Status | Not listed. No CISA KEV entry as of 2026-09-12. |
| CVSS Vector | CVSS: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 Versions | compression < 1.8.2 (all prior releases including 1.8.1, 1.8.0, 1.7.x, and earlier). |
| Fixed Version | compression 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 Date | 2026-09-11 via GitHub Security Advisory GHSA-vc2v-76pw-4v95. |
| Credits | Found by KKamJi98, fixed by UlisesGascon, reviewed by Phillip9587, analyzed by bjohansebas. |
noisgate verdict.
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.
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
compressionmiddleware 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.
What to do — in priority order.
- Offload compression to the reverse proxy or CDN — Configure nginx (
gzip on;), Cloudflare, or your cloud LB to handle response compression. Then remove or disableapp.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. - Rate-limit concurrent connections per source IP — Apply per-IP connection limits at your load balancer or WAF (e.g., nginx
limit_connat 50–100 per IP). This throttles the leak rate, making OOM impractical within auto-restart windows. - 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. - 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.
- Node.js
--max-old-space-sizeflag — 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-Encodingheaders. There is no malicious payload to signature-match.
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.
#!/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
fiIf you remember one thing.
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
What defenders are saying.
Crowdsourced verification outputs.
Results submitted by users who ran the verification payload against their environment.