← Back to Feed CACHED · 2026-07-18 13:29:38 · CACHE_KEY CVE-2026-15631
CVE-2026-15631 · CWE-22 · Disclosed 2026-07-18

Impact: @fastify/http-proxy versions from 9

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

The bouncer checks IDs at the front door but lets anyone in through the side WebSocket entrance

@fastify/http-proxy is a Fastify plugin that fronts internal services and enforces access boundaries by rewriting an external prefix onto a restricted upstream rewritePrefix. The HTTP path was hardened against traversal after CVE-2021-21322, but the WebSocket upgrade path was left using new URL(dest, upstream), which performs RFC-3986 resolution and collapses .. segments. That means a WS client sending /public/../admin/socket escapes the intended subtree and opens a bidirectional session against any upstream WebSocket endpoint the proxy can reach. Affected: 9.4.0 through 11.5.0 inclusive; fixed in 11.6.0.

Vendor severity of HIGH (8.7) is close, but slightly inflated. The exploit is unauth-remote (PR:N) with scope-change (S:C), which pushes CVSS up hard — however AC:H is legitimate because the attacker needs a deployment that (a) enables websocket: true, (b) uses rewritePrefix as an actual security boundary rather than a routing convenience, and (c) exposes hidden WS endpoints upstream. That's a real subset, not universal. Role still matters: this plugin *is* a trust boundary by design, so when the chain lands, it lands on the exact surface it was supposed to protect.

"Real WebSocket prefix-escape in a gateway plugin. Vendor 8.7 is slightly hot; HIGH still lands because the whole point of this plugin is being a trust boundary."
02 · The Attack Path

4 steps from start to impact.

STEP 01

Enumerate the proxy prefix

Attacker identifies a public Fastify endpoint fronted by @fastify/http-proxy with WebSocket support enabled. Reachability is confirmed by upgrading a benign path like wss://target/public/chat and observing a 101 Switching Protocols response. Fingerprinting is trivial via headers and behavior.
Conditions required:
  • Publicly reachable Fastify service
  • websocket: true in plugin options
  • Attacker can send raw WS handshakes (any browser or wscat)
Where this breaks in practice:
  • Deployments that terminate WS at a separate reverse proxy (nginx/Envoy) upstream of Fastify are not affected here
  • Many Fastify deployments use HTTP proxying only
Detection/coverage: Standard WAFs do not fingerprint this specific plugin; SBOM / Snyk / Dependabot will surface the vulnerable range
STEP 02

Craft path-traversal upgrade request

Using wscat or a scripted ws client, attacker issues GET /public/../internal/admin HTTP/1.1 with Upgrade: websocket. The plugin computes new URL('/public/../internal/admin', upstream), which resolves to upstream/internal/admin, bypassing the rewritePrefix restriction that would have been enforced for HTTP.
Conditions required:
  • Knowledge or guess of an internal WS route
  • Application uses rewritePrefix as an authorization boundary, not just a routing rewrite
Where this breaks in practice:
  • If the upstream has no WS handlers outside the intended prefix, the handshake fails
  • If rewritePrefix maps to a service that itself authenticates the WS upgrade, traversal doesn't grant access
Detection/coverage: Access logs will show the raw .. sequence in the request-line if not normalized upstream — grep for \.\./ in WS upgrade logs
STEP 03

Establish bidirectional session with hidden endpoint

Once the 101 completes against the escaped path, the attacker has a full WS session with an upstream endpoint the developer believed was reachable only via a trusted internal prefix. Typical juicy targets: admin control channels, metrics streams, GraphQL subscriptions with elevated scope, internal pub/sub bridges.
Conditions required:
  • Upstream WS endpoint exists at the traversed path
  • That endpoint trusts the proxy (no per-message authorization)
Where this breaks in practice:
  • Endpoints that require an authenticated session cookie the proxy doesn't forward remain protected
  • mTLS between proxy and upstream doesn't help — the proxy is the client
Detection/coverage: Application-layer WS auditing (message-count and identity per session) will show anomalous origin/prefix mismatch
STEP 04

Exfiltrate data or drive internal actions

Attacker consumes the WS stream (metrics, subscription payloads, admin events) or sends messages that the internal endpoint accepts as coming from a trusted proxy-side caller. Impact is bounded to whatever WS endpoints exist upstream — HTTP-only routes are not reachable via this bug.
Conditions required:
  • Endpoint accepts inbound messages without secondary auth
  • Sensitive data flows over WS
Where this breaks in practice:
  • Read-only telemetry streams have limited impact
  • Endpoints protected by JWT-per-message survive
Detection/coverage: Anomalous WS session duration or message rate from external ASN
03 · Intelligence Metadata

The supporting signals.

In-the-wild statusNo public exploitation observed as of 2026-07-18 (disclosure day); not KEV-listed
Proof-of-conceptAdvisory ships a minimal wscat reproducer inline (GHSA-7hrw-592w-9wh2); trivially weaponizable — expect PoC repos within days
EPSSNot yet scored — fresh CVE published 2026-07-18; expect low percentile initially given narrow applicability
KEV statusNot listed
CVSSCVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:H/A:N = 8.7 — S:C is the big driver; AC:H reflects the specific rewritePrefix-as-boundary prerequisite
Affected versions@fastify/http-proxy 9.4.0 → 11.5.0 inclusive
Fixed version11.6.0 — validates resolved WS path stays under rewritePrefix before forwarding
Related CVECVE-2021-21322 — same class of bug on the HTTP side of fastify-reply-from; this is the WS-cousin the earlier fix missed
Exposure datanpm weekly downloads of @fastify/http-proxy are in the low-hundreds-of-thousands; fraction using websocket: true with rewritePrefix as an auth boundary is a much smaller slice
ReporterMatteo Collina (mcollina, Fastify maintainer) — self-reported
04 · The Call

noisgate verdict.

Final Verdict
= UNCHANGED to HIGH (7.4/10)

HIGH holds because the affected component *is* the trust boundary — this plugin's job is enforcing rewritePrefix as an access edge, so a bypass hits exactly the surface it was deployed to protect. The single most decisive factor keeping this out of CRITICAL is scope containment: only WebSocket endpoints are reachable via the traversal, HTTP-only routes cannot complete the upgrade handshake.

HIGH Vulnerability mechanics and version range
MEDIUM Real-world exposed population using `rewritePrefix` as an auth boundary
LOW Exploitation velocity — CVE is < 24h old

Why this verdict

  • Trust-boundary role multiplier: @fastify/http-proxy is deployed specifically to enforce prefix-based isolation; the chain succeeds exactly where the defender expected protection — floor stays at HIGH
  • Unauth remote, no user interaction: any WS-capable client on the internet can throw the exploit; no phishing, no credential theft required
  • Downward pressure — HTTP paths unreachable: the S:C impact is bounded to what upstream WS handlers exist; deployments without sensitive WS endpoints upstream see functionally no impact, which is why 8.7 slightly overshoots
  • Downward pressure — deployment prerequisite is real: requires websocket: true AND rewritePrefix used as an authorization edge (not merely routing); a meaningful minority of installs, not the default
  • Role multiplier — high-value deployments: when this plugin fronts an internal admin/control WS bus (a common pattern), blast radius extends to whatever that bus can do — sufficient to hold the HIGH floor

Why not higher?

Not CRITICAL because impact is capped at reachable WebSocket endpoints only — the plugin still terminates HTTP path traversal correctly, so no fleet-scale or CA/hypervisor-class blast radius applies. Scope-change (S:C) is real but bounded to a single proxy tenant's upstream. No availability impact.

Why not lower?

Not MEDIUM because the affected component is by-design a trust boundary; downgrading it would ignore role. Unauth-remote plus scope-change on a gateway plugin, with a trivial wscat PoC included in the advisory, keeps this squarely in HIGH territory.

05 · Compensating Control

What to do — in priority order.

  1. Disable websocket: true where WS proxying isn't strictly required — Removes the entire vulnerable code path. Search your Fastify plugin registrations for require('@fastify/http-proxy') and set websocket: false (or omit) — deploy within 30 days per the noisgate HIGH mitigation SLA.
  2. Normalize .. in the upgrade path at the front edge — Configure your CDN/ingress (Cloudflare, nginx, Envoy, ALB) to reject or normalize .. sequences in the request-line of WS Upgrade requests before they reach Fastify. nginx: if ($request_uri ~ "\.\./") { return 400; }. Ship within 30 days.
  3. Add per-endpoint auth on upstream WS handlers — Stop treating rewritePrefix as a sufficient authorization boundary. Require a signed token on the WS handshake (Sec-WebSocket-Protocol or query param) that the upstream validates itself. This is the durable fix regardless of proxy version.
  4. Upgrade @fastify/http-proxy to ≥ 11.6.0 — The actual remediation — patches wsProxy to validate the resolved URL stays under rewritePrefix. Land within the noisgate HIGH remediation SLA of ≤ 180 days, prioritized where WS proxying is enabled.
  5. SBOM sweep and alert on unpatched consumers — Query your SCA/SBOM tooling (Snyk, Dependabot, Socket, GitHub Advisory) for @fastify/http-proxy in the 9.4.0–11.5.0 range across all repos and container images. Alert on new installs of vulnerable versions.
What doesn't work
  • WAF path-traversal rules on HTTP routes — the exploit rides the WS Upgrade handshake; most WAFs don't apply traversal signatures to WS upgrade request-lines by default
  • mTLS between proxy and upstream — the proxy is the trusted client; mutual TLS authenticates the connection, not the requested resource
  • Origin header checks in Fastify — this bug lives after origin validation; the traversal is in the resource path, not the Origin
  • Rate limiting — a single WS session is enough to exfiltrate; no volumetric signature to trip
06 · Verification

Crowdsourced verification payload.

Run this on any host or CI job with access to your repository/container-image tree. Invoke as ./check-fastify-http-proxy.sh /path/to/repo-or-node_modules. No elevated privileges required — reads package.json and node_modules metadata only.

noisgate-verify.sh
BASHREAD-ONLYSAFE
#!/usr/bin/env bash
# check-fastify-http-proxy.sh — CVE-2026-15631 detector
# Vulnerable: @fastify/http-proxy 9.4.0 <= x <= 11.5.0
# Fixed: 11.6.0
set -u

TARGET="${1:-.}"
STATUS="UNKNOWN"
FOUND=0
VULN=0

ver_le() { # returns 0 if $1 <= $2 (semver, x.y.z)
  [ "$1" = "$2" ] && return 0
  printf '%s\n%s\n' "$1" "$2" | sort -V -C 2>/dev/null && return 0 || return 1
}

in_range() { # 9.4.0 <= v <= 11.5.0
  local v="$1"
  ver_le "9.4.0" "$v" || return 1
  ver_le "$v" "11.5.0" || return 1
  return 0
}

# 1. Installed packages
while IFS= read -r pjson; do
  name=$(grep -oE '"name"[[:space:]]*:[[:space:]]*"[^"]+"' "$pjson" | head -1 | sed -E 's/.*"([^"]+)"$/\1/')
  ver=$(grep -oE '"version"[[:space:]]*:[[:space:]]*"[^"]+"' "$pjson" | head -1 | sed -E 's/.*"([^"]+)"$/\1/')
  if [ "$name" = "@fastify/http-proxy" ] || [ "$name" = "fastify-http-proxy" ]; then
    FOUND=1
    if in_range "$ver"; then
      echo "VULNERABLE install: $pjson  ($name@$ver)"
      VULN=1
    else
      echo "OK install: $pjson  ($name@$ver)"
    fi
  fi
done < <(find "$TARGET" -type f -name package.json -path '*/node_modules/*' 2>/dev/null)

# 2. Declared deps in root manifests / lockfiles
while IFS= read -r f; do
  if grep -qE '"@?fastify[-/]http-proxy"' "$f" 2>/dev/null; then
    echo "Declared dependency reference: $f"
    FOUND=1
  fi
done < <(find "$TARGET" -maxdepth 4 -type f \( -name package.json -o -name package-lock.json -o -name yarn.lock -o -name pnpm-lock.yaml \) 2>/dev/null | grep -v node_modules)

if [ "$VULN" = "1" ]; then
  STATUS="VULNERABLE"
elif [ "$FOUND" = "1" ]; then
  STATUS="UNKNOWN (dependency present — inspect resolved version)"
else
  STATUS="PATCHED (no @fastify/http-proxy in tree)"
fi

echo "---"
echo "STATUS: $STATUS"
case "$STATUS" in
  VULNERABLE*) exit 2 ;;
  UNKNOWN*)    exit 1 ;;
  *)           exit 0 ;;
esac
07 · Bottom Line

If you remember one thing.

TL;DR
Monday morning: run the SBOM sweep and inventory every repo/image that pulls @fastify/http-proxy 9.4.0–11.5.0 — filter to services with websocket: true, those are your real targets. Per the noisgate mitigation SLA for HIGH, ship a compensating control within 30 days: either flip websocket: false where WS isn't used, or add ..-normalization at your ingress edge (nginx/Envoy/CDN) for WS Upgrade requests. Per the noisgate remediation SLA for HIGH, upgrade to ≥ 11.6.0 within 180 days, prioritized where the plugin fronts admin, metrics, or pub/sub WebSocket endpoints. This is not a KEV/hair-on-fire event, but it is unauth-remote with a wscat-grade PoC embedded in the advisory — don't let it slide into next quarter.

Sources

  1. GHSA-7hrw-592w-9wh2 (advisory)
  2. fastify-http-proxy repository
  3. npm — @fastify/http-proxy
  4. CVE-2021-21322 (related HTTP-side variant)
  5. OpenJS Foundation Security Advisories
  6. Snyk — fastify package tree
  7. CWE-22 — Path Traversal
  8. Socket.dev — @fastify/http-proxy
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.