← Back to Feed CACHED · 2026-07-23 03:36:57 · CACHE_KEY CVE-2026-15074
CVE-2026-15074 · CWE-22 · Disclosed 2026-04-16

@fastify/static vulnerable to route guard bypass via path traversal

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

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.

"Route guard bypass in a Node.js static-file plugin. Only bites apps that put auth in front of @fastify/static and store secrets in the served dir."
02 · The Attack Path

4 steps from start to impact.

STEP 01

Recon the fastify app

Attacker fingerprints the target as Fastify (via 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.
Conditions required:
  • Target exposes a fastify HTTP endpoint on the network reachable by the attacker
  • Some route prefix has middleware-based access control
Where this breaks in practice:
  • Internal-only apps behind VPN/ZTNA are not reachable
  • API-only fastify deployments do not use @fastify/static at all
Detection/coverage: Web-app scanners (Burp, ZAP, Nuclei) don't flag Fastify-specific patterns by default; SCA tools (Snyk, Dependabot, GitHub Advisory) catch the package version directly.
STEP 02

Enumerate a static mount protected by a route guard

Attacker probes for a @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.
Conditions required:
  • Application uses fastify.register(fastifyStatic, { prefix: '/admin/' }) or similar
  • The served directory contains sensitive artifacts (build manifests, source maps, tenant assets)
Where this breaks in practice:
  • Most fastify apps mount static at /public or /assets with no guard in front — no bypass to perform
  • Sensitive data is usually served through DB-backed handlers, not filesystem static
Detection/coverage: No IDS signature for this — it looks like normal HTTP GETs.
STEP 03

Craft the encoded-separator bypass

Attacker sends 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.
Conditions required:
  • Direct HTTP reachability with no path-normalizing proxy in front
  • No WAF rule stripping or rejecting %2F in path
Where this breaks in practice:
  • CloudFront, Cloudflare, nginx, and most reverse proxies normalize %2F to / before origin, which restores the guard match and kills the bypass
  • AWS ALB and many API gateways reject %2F in path by default
Detection/coverage: Access logs will show requests with literal %2F in the path — trivial to grep for retroactively.
STEP 04

Exfiltrate whatever the static dir exposes

Attacker pulls whatever files sit under the guarded prefix's static root — typical spoils are internal HTML dashboards, JS source maps, .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.
Conditions required:
  • Sensitive files actually exist inside the served directory
Where this breaks in practice:
  • 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
Detection/coverage: DLP and egress monitoring may catch large asset pulls; a spike in 200s to %2F-bearing URLs is a strong signal.
03 · Intelligence Metadata

The supporting signals.

In-the-wild exploitationNone observed as of 2026-07-23. No GreyNoise tag, no Censys/Shodan campaign fingerprint.
KEV statusNot listed in CISA KEV.
EPSSLow (<1st percentile) — expected for a niche Node.js library bypass.
Proof of conceptPoC in the GHSA-x428-ghpx-8j92 advisory itself: single curl with %2F in path. No weaponized exploit kit.
CVSS reality checkIntel block says 7.5 AC:L; GHSA and NVD-aligned score is 5.9 AC:H. AC:H is correct — needs specific app topology.
CWECWE-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 version9.1.1 (npm). No distro backports — this is npm-only.
DisclosureGHSA 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.
04 · The Call

noisgate verdict.

Final Verdict
DOWNGRADED to MEDIUM (5.9/10)

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.

HIGH on the technical mechanism and affected versions
HIGH on absence of active exploitation
MEDIUM on real-world exposure population — no SCA telemetry vendor has published a fastify+static+guard breakdown

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 %2F to / 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 /public with 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/static is 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.

05 · Compensating Control

What to do — in priority order.

  1. Upgrade @fastify/static to 9.1.1+ — The only complete fix. npm ls @fastify/static across 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.
  2. Reject %2F (and %5C on 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.
  3. Move sensitive files out of any @fastify/static root — 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.
  4. 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.
What doesn't work
  • 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/static directory listing — the bypass fetches specific files by name, listing was never involved.
  • IP allowlisting the /admin route — 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.
06 · Verification

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.

noisgate-verify.sh
BASHREAD-ONLYSAFE
#!/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
07 · Bottom Line

If you remember one thing.

TL;DR
Monday morning: run 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

  1. GitHub Security Advisory GHSA-x428-ghpx-8j92
  2. GitHub Advisory Database CVE-2026-6414
  3. GitLab Advisory Database CVE-2026-6414
  4. SentinelOne vulnerability DB CVE-2026-6414
  5. Related: @fastify/static directory listing traversal (GHSA-pr96-94w5-mx2h)
  6. Snyk: Directory Traversal in @fastify/static (CVE-2026-6410)
  7. fastify-static security overview
  8. CWE-177: Improper Handling of URL Encoding
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.