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.
4 steps from start to impact.
Discover a Fastify app serving gated static content
@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.- Application uses @fastify/static <=9.1.0
- Application uses a Fastify preHandler / hook / plugin to gate a static subtree
- 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
@fastify/static <9.1.1 in SBOMsCraft the encoded-separator payload
%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.- HTTP/1.1 or HTTP/2 client that does not pre-normalize the URL
- No upstream proxy that decodes and re-canonicalizes
- Cloudflare, Akamai, and Fastly aggressively normalize
%2Fin the path before forwarding - AWS ALB and modern NGINX (with
merge_slashes/normalize_uri) can be configured to reject or decode encoded slashes
%2F sequences at low confidenceBypass the Fastify route guard
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.- Guard is registered at the Fastify router layer, not at the filesystem layer
- If guard is a
onRequestglobal 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
200 for a request whose path contains %2F — easy hunt queryStatic 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.- Sensitive file lives INSIDE the configured
rootdirectory
- Well-architected apps keep secrets out of any static root
- This is
C:L— one file at a time, no lateral movement primitive
The supporting signals.
| In-the-wild exploitation | None observed. Not in CISA KEV. No named campaigns, no incident-response reports. |
|---|---|
| Proof-of-concept | Trivial 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. |
| EPSS | Expected <0.5% / <30th percentile — consistent with unauthenticated MEDIUM confidentiality-only npm library bugs. Confirm at FIRST EPSS. |
| KEV status | Not listed. No CISA action required. |
| CVSS vector | AV: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. |
| Disclosure | 2026 npm advisory chain following the earlier CVE-2026-6414 pattern. Same reporter class (Fastify security WG). |
| Reporter | Fastify security working group / community researcher (per GHSA convention). |
noisgate verdict.
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.
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/staticinstalls. - Role multiplier: none.
@fastify/staticis 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.
What to do — in priority order.
- Upgrade
@fastify/staticto >= 9.1.1 — The only real fix. Bump the pin inpackage.json, runnpm 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. - 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. - 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. - Convert route-scoped
preHandlerguards to globalonRequesthooks — 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.
- 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%2Fdecoding. - 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...
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).
#!/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
If you remember one thing.
@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
- GHSA-x428-ghpx-8j92 — sibling advisory in @fastify/static
- GitHub Advisory DB — CVE-2026-6414 (same class)
- SentinelOne — CVE-2026-6414 writeup
- Snyk — Directory Traversal in @fastify/static
- GitLab Advisory DB — @fastify/static CVE-2026-6414
- Fastify security advisories index
- CWE-180 — Incorrect Behavior Order: Validate Before Canonicalize
- FIRST EPSS API — CVE-2026-7120
What defenders are saying.
Crowdsourced verification outputs.
Results submitted by users who ran the verification payload against their environment.