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.
4 steps from start to impact.
Enumerate the proxy prefix
@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.- Publicly reachable Fastify service
websocket: truein plugin options- Attacker can send raw WS handshakes (any browser or
wscat)
- 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
Craft path-traversal upgrade request
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.- Knowledge or guess of an internal WS route
- Application uses
rewritePrefixas an authorization boundary, not just a routing rewrite
- If the upstream has no WS handlers outside the intended prefix, the handshake fails
- If
rewritePrefixmaps to a service that itself authenticates the WS upgrade, traversal doesn't grant access
.. sequence in the request-line if not normalized upstream — grep for \.\./ in WS upgrade logsEstablish bidirectional session with hidden endpoint
- Upstream WS endpoint exists at the traversed path
- That endpoint trusts the proxy (no per-message authorization)
- 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
Exfiltrate data or drive internal actions
- Endpoint accepts inbound messages without secondary auth
- Sensitive data flows over WS
- Read-only telemetry streams have limited impact
- Endpoints protected by JWT-per-message survive
The supporting signals.
| In-the-wild status | No public exploitation observed as of 2026-07-18 (disclosure day); not KEV-listed |
|---|---|
| Proof-of-concept | Advisory ships a minimal wscat reproducer inline (GHSA-7hrw-592w-9wh2); trivially weaponizable — expect PoC repos within days |
| EPSS | Not yet scored — fresh CVE published 2026-07-18; expect low percentile initially given narrow applicability |
| KEV status | Not listed |
| CVSS | CVSS: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 version | 11.6.0 — validates resolved WS path stays under rewritePrefix before forwarding |
| Related CVE | CVE-2021-21322 — same class of bug on the HTTP side of fastify-reply-from; this is the WS-cousin the earlier fix missed |
| Exposure data | npm 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 |
| Reporter | Matteo Collina (mcollina, Fastify maintainer) — self-reported |
noisgate verdict.
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.
Why this verdict
- Trust-boundary role multiplier:
@fastify/http-proxyis 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: trueANDrewritePrefixused 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.
What to do — in priority order.
- Disable
websocket: truewhere WS proxying isn't strictly required — Removes the entire vulnerable code path. Search your Fastify plugin registrations forrequire('@fastify/http-proxy')and setwebsocket: false(or omit) — deploy within 30 days per the noisgate HIGH mitigation SLA. - 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 WSUpgraderequests before they reach Fastify. nginx:if ($request_uri ~ "\.\./") { return 400; }. Ship within 30 days. - Add per-endpoint auth on upstream WS handlers — Stop treating
rewritePrefixas a sufficient authorization boundary. Require a signed token on the WS handshake (Sec-WebSocket-Protocolor query param) that the upstream validates itself. This is the durable fix regardless of proxy version. - Upgrade
@fastify/http-proxyto ≥ 11.6.0 — The actual remediation — patcheswsProxyto validate the resolved URL stays underrewritePrefix. Land within the noisgate HIGH remediation SLA of ≤ 180 days, prioritized where WS proxying is enabled. - SBOM sweep and alert on unpatched consumers — Query your SCA/SBOM tooling (Snyk, Dependabot, Socket, GitHub Advisory) for
@fastify/http-proxyin the 9.4.0–11.5.0 range across all repos and container images. Alert on new installs of vulnerable versions.
- WAF path-traversal rules on HTTP routes — the exploit rides the WS
Upgradehandshake; 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
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.
#!/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
If you remember one thing.
@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
What defenders are saying.
Crowdsourced verification outputs.
Results submitted by users who ran the verification payload against their environment.