The bouncer checks IDs at the front door, but the coat check hands out coats without asking
@fastify/static percent-decodes path separators (%2F) *before* handing the resolved path to the filesystem, while Fastify's core router matches the raw, still-encoded URL. That mismatch means a request like /admin%2Fsecret.html sails past any preHandler/onRequest guard bound to /admin/* — the router sees a single opaque path segment — but the static plugin cheerfully decodes it and serves admin/secret.html from disk. Affected versions are 8.0.0 through 9.1.0; the fix landed in 9.1.1. Root cause is CWE-177 (improper URL-encoding handling), not classic ../ traversal — you cannot escape the served root, you can only reach files *inside* it that middleware was supposed to gate.
The vendor CVSS in the intel block (7.5, AV:N/AC:L/PR:N/UI:N/C:H) is out of sync with the GitHub Security Advisory, which scores this 5.9 with AC:H because the exploit requires a very specific app topology: route-based auth in front of a @fastify/static mount whose served directory contains files the guard was meant to protect. That is a real but narrow pattern. Confidentiality-only impact, no integrity or availability effect. Treat this as MEDIUM unless you have telemetry proving the vulnerable pattern is in your stack.
4 steps from start to impact.
Recon the fastify app
x-powered-by, error page shape, or JS bundle references to @fastify/static) and identifies a protected route prefix such as /admin/* or /internal/*. Public GitHub search and Wappalyzer-style tooling give this away trivially for OSS or SaaS deployments.- Target exposes a fastify HTTP endpoint on the network reachable by the attacker
- Some route prefix has middleware-based access control
- Internal-only apps behind VPN/ZTNA are not reachable
- API-only fastify deployments do not use
@fastify/staticat all
Enumerate a static mount protected by a route guard
@fastify/static mount that both (a) sits underneath a guarded prefix and (b) serves files the guard was meant to hide. Public issue trackers, source leaks, and dirbusting with wordlists (ffuf, feroxbuster) map the mount points.- Application uses
fastify.register(fastifyStatic, { prefix: '/admin/' })or similar - The served directory contains sensitive artifacts (build manifests, source maps, tenant assets)
- Most fastify apps mount static at
/publicor/assetswith no guard in front — no bypass to perform - Sensitive data is usually served through DB-backed handlers, not filesystem static
Craft the encoded-separator bypass
GET /admin%2Fsecret.html (or %2f, %252F variants depending on any upstream proxy behavior). Fastify's router evaluates the raw string — no match on /admin/* because the URL is a single segment — so the preHandler auth guard never runs. @fastify/static then decodes and resolves admin/secret.html relative to its root and returns the file body.- Direct HTTP reachability with no path-normalizing proxy in front
- No WAF rule stripping or rejecting
%2Fin path
- CloudFront, Cloudflare, nginx, and most reverse proxies normalize
%2Fto/before origin, which restores the guard match and kills the bypass - AWS ALB and many API gateways reject
%2Fin path by default
%2F in the path — trivial to grep for retroactively.Exfiltrate whatever the static dir exposes
.env files accidentally shipped to the assets dir, tenant-scoped uploads, or admin UI templates. No RCE, no write, no lateral movement — just confidentiality loss bounded by what's on disk.- Sensitive files actually exist inside the served directory
- Well-run deployments do not keep secrets in static-served directories
- Blast radius is one app, one tenant, one filesystem subtree — no fleet or identity impact
%2F-bearing URLs is a strong signal.The supporting signals.
| In-the-wild exploitation | None observed as of 2026-07-23. No GreyNoise tag, no Censys/Shodan campaign fingerprint. |
|---|---|
| KEV status | Not listed in CISA KEV. |
| EPSS | Low (<1st percentile) — expected for a niche Node.js library bypass. |
| Proof of concept | PoC in the GHSA-x428-ghpx-8j92 advisory itself: single curl with %2F in path. No weaponized exploit kit. |
| CVSS reality check | Intel block says 7.5 AC:L; GHSA and NVD-aligned score is 5.9 AC:H. AC:H is correct — needs specific app topology. |
| CWE | CWE-177 (Improper Handling of URL Encoding). *Not* classic CWE-22 directory traversal — you cannot break out of the served root. |
| Affected versions | @fastify/static >= 8.0.0, <= 9.1.0 |
| Fixed version | 9.1.1 (npm). No distro backports — this is npm-only. |
| Disclosure | GHSA published 2026-04-16, reported by blakeembrey. |
| Exposure population | @fastify/static ~2M weekly npm downloads, but only a small fraction combines it with route-guard middleware over sensitive filesystem content. |
noisgate verdict.
MEDIUM because the decisive constraint is a narrow deployment pattern — the app must both mount @fastify/static under a guarded prefix and store sensitive files inside that served directory, which is a small minority of real deployments. Confidentiality-only impact with no path to code execution, integrity loss, or lateral movement caps the ceiling.
Why this verdict
- Vendor score reconciliation: the 7.5 AV:N/AC:L in the intel block conflicts with the GHSA's 5.9 AC:H. AC:H is defensible because the exploit demands a specific app shape (guard + static mount + sensitive files in served dir). Adopt 5.9 as the baseline.
- Friction — reverse proxy normalization: most production Node apps sit behind nginx, Cloudflare, AWS ALB, or a similar edge that normalizes
%2Fto/before origin, which restores the router match and neutralizes the bypass. This alone eliminates a large share of the theoretically-vulnerable population. - Friction — application pattern rarity: the vulnerable pattern (route-guard-protected static mount over a directory containing genuinely sensitive files) is uncommon; most fastify shops mount static at
/publicwith no guard, or serve gated content through DB-backed handlers, not the filesystem. - Role multiplier — low-value role (LOB web app): chain succeeds; blast radius = read a subset of files from one static mount. No privilege gain, no lateral movement. Confidentiality only.
- Role multiplier — typical role (customer-facing SaaS tier): chain succeeds; blast radius = tenant-scoped file exposure if the static mount is per-tenant. Still confidentiality-only, still single-app.
- Role multiplier — high-value role (identity provider / CI / backup / hypervisor):
@fastify/staticis not the identity, secrets, or orchestration plane. There is no realistic high-value-role deployment where this plugin's blast radius escalates to fleet, domain, or supply-chain compromise. No role floor applies — the HIGH/CRITICAL floor from the deployment-role rule is not triggered. - Impact ceiling: vector explicitly says I:N/A:N. No write, no RCE, no DoS. Confidentiality alone with a narrow file set caps this at MEDIUM.
Why not higher?
HIGH would require either fleet-scale blast radius (this bug reads files from one mount on one app), an identity/trust role multiplier (fastify-static is not in that catalog), or active exploitation/KEV listing (neither present). AC:H plus proxy normalization plus rare vulnerable pattern make HIGH indefensible.
Why not lower?
LOW would ignore that when the vulnerable pattern *does* exist — guarded admin static mount serving real secrets — the bypass is a single curl with no auth. It is a real access-control failure with confirmed PoC and a clean vendor patch, so it deserves a scheduled remediation, not backlog hygiene.
What to do — in priority order.
- Upgrade
@fastify/staticto 9.1.1+ — The only complete fix.npm ls @fastify/staticacross your monorepos and CI images, bump direct dependencies, and let renovate/dependabot carry transitive bumps. Deploy within the noisgate remediation window (365 days for MEDIUM) — but if you have a confirmed vulnerable pattern, treat it as urgent and ship this sprint. - Reject
%2F(and%5Con Windows) in URL paths at the edge — Add a WAF/nginx rule:if ($request_uri ~* "%2[fF]") { return 400; }or the Cloudflare/AWS WAF equivalent. This kills the bypass regardless of the plugin version. Since there is no mitigation SLA at MEDIUM, deploy opportunistically alongside your next WAF ruleset push, but this is cheap enough to do now. - Move sensitive files out of any
@fastify/staticroot — Serve gated content through an explicit handler that runs the auth check in code (fastify.get('/admin/secret.html', {preHandler: auth}, ...)), not through filesystem static. This is defense-in-depth and fixes the whole class of routing/serving mismatches, not just this CVE. - Enable path normalization on all fronting proxies — Confirm nginx
merge_slashes on;, Cloudflare 'Normalize URLs to origin' setting, and AWS ALB path-normalization behavior are on. Any of these individually neutralizes the exploit for traffic that traverses them.
- Adding a
../blacklist — this bug is not classic directory traversal; you cannot escape the served root, so../filters miss the point entirely. - Turning off
@fastify/staticdirectory listing — the bypass fetches specific files by name, listing was never involved. - IP allowlisting the
/adminroute — the guard is exactly what gets bypassed; the request never matches/admin/*at the router, so an IP-check preHandler bound to that prefix also never runs. - EDR / host IDS — this is a legitimate HTTP request producing a legitimate file read by the node process. Endpoint tooling has no signal.
Crowdsourced verification payload.
Run on any host with Node.js and network access to the target app, or point it at a checked-out repo. Usage: ./check.sh https://app.example.com /admin (probes for the live bypass) or ./check.sh --package ./package-lock.json (SCA check). No privileges required.
#!/usr/bin/env bash
# noisgate verifier for CVE-2026-6414 (@fastify/static route guard bypass)
# Exit codes: 0=PATCHED, 1=VULNERABLE, 2=UNKNOWN
set -u
usage() { echo "usage: $0 <base_url> <guarded_prefix> | $0 --package <path/to/package-lock.json>" >&2; exit 2; }
[ $# -lt 2 ] && usage
if [ "$1" = "--package" ]; then
LOCK="$2"
[ ! -f "$LOCK" ] && { echo "UNKNOWN: lockfile not found"; exit 2; }
VER=$(grep -Eo '"@fastify/static"[^}]*"version": "[0-9]+\.[0-9]+\.[0-9]+"' "$LOCK" | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+' | sort -uV | tail -1)
if [ -z "$VER" ]; then echo "PATCHED: @fastify/static not present in $LOCK"; exit 0; fi
# semver compare: vulnerable if >=8.0.0 and <=9.1.0
MIN="8.0.0"; MAX="9.1.0"
lowest=$(printf '%s\n%s\n' "$VER" "$MIN" | sort -V | head -1)
highest=$(printf '%s\n%s\n' "$VER" "$MAX" | sort -V | tail -1)
if [ "$lowest" = "$MIN" ] && [ "$highest" = "$MAX" ]; then
echo "VULNERABLE: @fastify/static $VER is in range >=8.0.0,<=9.1.0"; exit 1
else
echo "PATCHED: @fastify/static $VER is outside vulnerable range"; exit 0
fi
fi
BASE="${1%/}"
PREFIX="${2#/}"; PREFIX="${PREFIX%/}"
PROBE="probe-$(date +%s).html"
# Baseline: guarded path should be 401/403/404
BLOCKED=$(curl -sk -o /dev/null -w '%{http_code}' "$BASE/$PREFIX/$PROBE")
# Bypass attempt: %2F between prefix and file
BYPASS=$(curl -sk -o /dev/null -w '%{http_code}' "$BASE/${PREFIX}%2F$PROBE")
echo "guarded response: $BLOCKED bypass response: $BYPASS"
if [ "$BLOCKED" != "200" ] && [ "$BYPASS" = "200" ]; then
echo "VULNERABLE: route guard bypassed via %2F"; exit 1
fi
if [ "$BLOCKED" = "$BYPASS" ]; then
echo "PATCHED: guard behavior identical for encoded and decoded separator"; exit 0
fi
echo "UNKNOWN: inconclusive — run against a known-guarded, known-existing static file"; exit 2
If you remember one thing.
npm ls @fastify/static across every Node repo and container image, and flag anything in the >=8.0.0, <=9.1.0 band. Because the verdict is MEDIUM there is no noisgate mitigation SLA — but the WAF rule to reject %2F in path is a 15-minute change with negligible risk, so ship it this sprint anyway. Bump to 9.1.1+ within the noisgate remediation SLA of 365 days; pull it forward to this quarter for any app that (a) uses @fastify/static mounted under an auth-guarded prefix AND (b) keeps files in that directory that the guard was intended to protect. If neither condition holds, this is dependency-hygiene backlog, not an incident.Sources
- GitHub Security Advisory GHSA-x428-ghpx-8j92
- GitHub Advisory Database CVE-2026-6414
- GitLab Advisory Database CVE-2026-6414
- SentinelOne vulnerability DB CVE-2026-6414
- Related: @fastify/static directory listing traversal (GHSA-pr96-94w5-mx2h)
- Snyk: Directory Traversal in @fastify/static (CVE-2026-6410)
- fastify-static security overview
- CWE-177: Improper Handling of URL Encoding
What defenders are saying.
Crowdsourced verification outputs.
Results submitted by users who ran the verification payload against their environment.