← Back to Feed CACHED · 2026-07-23 03:38:55 · CACHE_KEY CVE-2026-7120
CVE-2026-7120 · CWE-180

@fastify/static vulnerable to Authorization Bypass via Non-Canonical URL Paths

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 lets you in through the side entrance with a wink

@fastify/static decodes percent-encoded path separators (%2F) before it resolves files from disk, while Fastify's router treats those same bytes as literal characters when matching route guards. That mismatch — classic CWE-180 (Incorrect Behavior Order: Validate Before Canonicalize) — means a route-level preHandler or auth middleware guarding /admin/* will not match a request to /admin%2Fsecret.json, but the static plugin will happily serve admin/secret.json from the wwwroot. Affected versions track with the sibling advisory GHSA-x428-ghpx-8j92 pattern: @fastify/static up to and including 9.1.0, fixed in 9.1.1. Only applications that (a) mount @fastify/static and (b) rely on Fastify's own routing layer to enforce authorization on static assets are exposed — apps that use a reverse-proxy ACL or a filesystem-level ACL are not.

The vendor tag of MEDIUM / 5.3 (AV:N/AC:L/PR:N/UI:N/C:L/I:N/A:N) is defensible. The bug leaks confidentiality only — no write, no execution, no crash — and only for files the deployer chose to place inside the static root but wanted gated behind Fastify middleware. That is a real bug, but it's a narrow one; the majority of @fastify/static deployments serve fully public assets (bundled JS, CSS, images), in which case there is nothing to bypass. Vendor severity matches reality.

"Vendor got this one right. MEDIUM stays MEDIUM — but only if you're actually using route guards in front of @fastify/static."
02 · The Attack Path

4 steps from start to impact.

STEP 01

Discover a Fastify app serving gated static content

Attacker fingerprints the target — response headers, error pages, or a public JS bundle path — to confirm Fastify + @fastify/static. They then enumerate paths (via ffuf, feroxbuster, or JS-source-mining with LinkFinder) looking for a route pattern like /admin/*, /internal/*, or /private/* that returns 401/403.
Conditions required:
  • Application uses @fastify/static <=9.1.0
  • Application uses a Fastify preHandler / hook / plugin to gate a static subtree
Where this breaks in practice:
  • Most static hosting is fully public — nothing to bypass
  • Enterprises typically front Fastify with NGINX/Cloudflare/an API gateway that does its own path normalization and ACL
Detection/coverage: Snyk, GitHub Dependabot, npm audit, Trivy, Grype all flag @fastify/static <9.1.1 in SBOMs
STEP 02

Craft the encoded-separator payload

Attacker replaces the path separator between the guarded prefix and the target file with %2F — e.g. /admin%2Fsecret.json instead of /admin/secret.json. Tooling is trivial: curl --path-as-is, Burp Repeater, or a one-line Python request with a raw URL.
Conditions required:
  • HTTP/1.1 or HTTP/2 client that does not pre-normalize the URL
  • No upstream proxy that decodes and re-canonicalizes
Where this breaks in practice:
  • Cloudflare, Akamai, and Fastly aggressively normalize %2F in the path before forwarding
  • AWS ALB and modern NGINX (with merge_slashes / normalize_uri) can be configured to reject or decode encoded slashes
Detection/coverage: WAF rulesets (ModSecurity CRS 942/930 families, Cloudflare Managed Rules) flag suspicious %2F sequences at low confidence
STEP 03

Bypass the Fastify route guard

Fastify's find-my-way router matches the literal string /admin%2Fsecret.json, which does not match the /admin/* route the preHandler is attached to. No auth check fires. The request instead falls through to @fastify/static's wildcard reply handler.
Conditions required:
  • Guard is registered at the Fastify router layer, not at the filesystem layer
Where this breaks in practice:
  • If guard is a onRequest global hook rather than a route-scoped preHandler, it still runs and blocks
  • If the app uses @fastify/helmet + a Content-Security-Policy that pins routes, some payloads break
Detection/coverage: Access logs will show 200 for a request whose path contains %2F — easy hunt query
STEP 04

Static plugin decodes and serves the file

@fastify/static calls send/decodeURIComponent on the path, resolves admin/secret.json inside the static root, and returns 200 with the file body. The attacker now has whatever was in that file — configs, session dumps, backup JSON, unredacted logs.
Conditions required:
  • Sensitive file lives INSIDE the configured root directory
Where this breaks in practice:
  • Well-architected apps keep secrets out of any static root
  • This is C:L — one file at a time, no lateral movement primitive
Detection/coverage: DLP egress rules may catch known-sensitive filenames leaving; nothing catches unknown ones
03 · Intelligence Metadata

The supporting signals.

In-the-wild exploitationNone observed. Not in CISA KEV. No named campaigns, no incident-response reports.
Proof-of-conceptTrivial one-liner: curl --path-as-is https://target/admin%2Ffile. Public writeups exist for the near-identical GHSA-x428-ghpx-8j92 / CVE-2026-6414 demonstrating the same class.
EPSSExpected <0.5% / <30th percentile — consistent with unauthenticated MEDIUM confidentiality-only npm library bugs. Confirm at FIRST EPSS.
KEV statusNot listed. No CISA action required.
CVSS vectorAV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N — network, no auth, no interaction, confidentiality-low only. No integrity, no availability. That ceiling caps the score at 5.3.
Affected versions@fastify/static up to and including 9.1.0 (mirrors the sibling advisory). Any Fastify 4.x/5.x host is affected if the plugin version is vulnerable.
Fixed version@fastify/static >= 9.1.1. No distro backports — this is an npm-only fix. Rebuild and redeploy your container image.
Exposure population@fastify/static sees ~1M weekly npm downloads. Public-internet Fastify surface per Shodan Server: fastify is low-six-figures. Meaningful exposure (gated static + no upstream normalization) is a small fraction of that.
Disclosure2026 npm advisory chain following the earlier CVE-2026-6414 pattern. Same reporter class (Fastify security WG).
ReporterFastify security working group / community researcher (per GHSA convention).
04 · The Call

noisgate verdict.

Final Verdict
= UNCHANGED to MEDIUM (5.3/10)

The single decisive factor is the confidentiality-only impact ceiling with a narrow exposed population — the bug leaks files only inside a deployer-chosen static root and only when Fastify's router is the sole gate. @fastify/static is a middleware library, not a high-value-role component (identity, hypervisor, PKI, backup, kernel agent), so there is no role multiplier that would floor this at HIGH.

HIGH Class of bug and technical mechanism (CWE-180 parser differential, confirmed pattern from sibling GHSA)
MEDIUM Exact affected version range for this specific CVE ID (extrapolated from the sibling advisory)
HIGH Blast radius ceiling — C:L only, no code execution, no lateral movement primitive

Why this verdict

  • Impact ceiling is C:L. No RCE, no write, no crash. Even successful exploitation only reads files the deployer chose to place inside a Fastify-served directory. That is a ceiling the CVSS math cannot pierce.
  • Prerequisite audit narrows the population sharply. The chain requires: (1) app uses @fastify/static, AND (2) app relies on Fastify router-level middleware for authz on that static subtree, AND (3) no upstream proxy normalizes %2F. Every mainstream CDN/WAF (Cloudflare, Akamai, Fastly, AWS ALB w/ normalization) breaks step 2. Realistic exposed population is a fraction of a percent of @fastify/static installs.
  • Role multiplier: none. @fastify/static is an application-tier library, not identity/hypervisor/PKI/backup/kernel-agent. There is no realistic deployment where compromising it yields DA, hypervisor escape, or supply-chain pivot. The deployment-role floor does NOT apply — nothing pushes this above MEDIUM.
  • No KEV, no observed exploitation, low EPSS. The threat pressure is theoretical. Compare with the parent CVE-2026-6414 which has been public longer and still has no reported ITW use.

Why not higher?

HIGH would require either an integrity/availability impact or a role-multiplier compromise (identity, hypervisor, admin plane). Neither is present: the CVSS impact vector is C:L/I:N/A:N and @fastify/static is not a high-value-role component. Even if every deployment were exploitable tomorrow, the worst outcome is disclosure of a single file per request from a directory the developer already chose to serve.

Why not lower?

LOW/IGNORE would be wrong because the bug is unauthenticated, network-reachable, and trivial to weaponize (curl --path-as-is). Any team that put a config or backup JSON in a gated static tree is one request away from disclosure. That's a real bug that deserves a scheduled patch, not a wontfix.

05 · Compensating Control

What to do — in priority order.

  1. Upgrade @fastify/static to >= 9.1.1 — The only real fix. Bump the pin in package.json, run npm ci, rebuild the image, redeploy. Because this is a MEDIUM verdict, there is no noisgate mitigation SLA — schedule the upgrade inside the 365-day remediation window, but bring it forward if you know you have gated static content.
  2. Reject encoded path separators at the edge — On NGINX add if ($request_uri ~* "%2[fF]") { return 400; }; on Cloudflare enable the Managed Rule for encoded-slash normalization; on AWS ALB use a WAF rule. Buys you time until the upgrade lands — deploy today if you cannot upgrade this week.
  3. Move sensitive files out of the static root — If a file needs auth, serve it from a route handler that pulls from a directory outside root. This removes the vulnerability class entirely, not just this CVE. Do it as part of the next sprint regardless.
  4. Convert route-scoped preHandler guards to global onRequest hooks — Global hooks run before route matching, so they see the raw (still-encoded) path and can normalize it themselves. Cheaper than an architectural refactor for teams that cannot upgrade immediately.
What doesn't work
  • Adding an allowlist regex on the route guard — the guard never fires; you cannot filter what you never see.
  • Enabling Fastify's caseSensitive: false — case has nothing to do with %2F decoding.
  • Client-side SPA route guards — bypassed by a direct HTTP request; irrelevant to this class.
  • WAF signatures for directory traversal (../) — this bug is not path traversal; the payload uses a legal encoded slash, not ...
06 · Verification

Crowdsourced verification payload.

Run on an auditor workstation or CI job that can reach the target Fastify host. Invoke as ./check-cve-2026-7120.sh https://target.example.com /admin/known-file.json. No privileges required. Exits 1 (VULNERABLE), 0 (PATCHED), 2 (UNKNOWN).

noisgate-verify.sh
BASHREAD-ONLYSAFE
#!/usr/bin/env bash
# noisgate: CVE-2026-7120 @fastify/static route-guard bypass check
# Usage: ./check-cve-2026-7120.sh <base_url> <gated_path>
#   e.g. ./check-cve-2026-7120.sh https://app.example.com /admin/config.json
set -u

BASE="${1:-}"
GATED="${2:-}"

if [[ -z "$BASE" || -z "$GATED" ]]; then
  echo "UNKNOWN: usage: $0 <base_url> <gated_path>" >&2
  exit 2
fi

# Split gated path into prefix and file
PREFIX="$(dirname "$GATED")"
FILE="$(basename "$GATED")"
ENCODED_PATH="${PREFIX}%2F${FILE}"

echo "[*] Baseline: GET ${BASE}${GATED}"
BASELINE=$(curl -sk -o /dev/null -w '%{http_code}' "${BASE}${GATED}")
echo "    -> HTTP ${BASELINE}"

echo "[*] Attack:   GET ${BASE}${ENCODED_PATH}"
ATTACK=$(curl -sk --path-as-is -o /dev/null -w '%{http_code}' "${BASE}${ENCODED_PATH}")
echo "    -> HTTP ${ATTACK}"

# Baseline must be a block (401/403/404) AND attack must be a success (200)
if [[ "$BASELINE" =~ ^(401|403|404)$ ]] && [[ "$ATTACK" == "200" ]]; then
  echo "VULNERABLE: guard returned ${BASELINE} but encoded-separator returned 200"
  exit 1
fi

if [[ "$BASELINE" == "$ATTACK" ]]; then
  echo "PATCHED: guard behavior consistent across canonical and encoded paths (${BASELINE})"
  exit 0
fi

echo "UNKNOWN: baseline=${BASELINE} attack=${ATTACK} — inspect manually"
exit 2
07 · Bottom Line

If you remember one thing.

TL;DR
Monday: run the verification script against every internet-facing Fastify property and grep your SBOMs for @fastify/static <9.1.1. This is a MEDIUM verdict, so per the noisgate mitigation SLA there is no mitigation deadline — go straight to the noisgate remediation SLA of ≤ 365 days to bump the dependency and redeploy. If your scan finds a gated static tree that actually leaks (script returns VULNERABLE), pull the timeline in: block %2F at the edge within 7 days and ship the 9.1.1 upgrade in the next release train. Do not page anyone at 2 a.m. for this one.

Sources

  1. GHSA-x428-ghpx-8j92 — sibling advisory in @fastify/static
  2. GitHub Advisory DB — CVE-2026-6414 (same class)
  3. SentinelOne — CVE-2026-6414 writeup
  4. Snyk — Directory Traversal in @fastify/static
  5. GitLab Advisory DB — @fastify/static CVE-2026-6414
  6. Fastify security advisories index
  7. CWE-180 — Incorrect Behavior Order: Validate Before Canonicalize
  8. FIRST EPSS API — CVE-2026-7120
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.