Your route guard checks the spelling on the invitation, but Windows doesn't care about uppercase
CVE-2026-90982 is the sixth bypass variant in a rolling series of @fastify/static route-guard / allowedPath evasion bugs disclosed since April 2026. This one exploits CWE-178 (Improper Handling of Case Sensitivity): on case-insensitive filesystems (Windows, default macOS), a request to /DEEP/secret.txt does not match the guarded /deep/* route in Fastify's router (find-my-way), so the allowedPath callback never fires — yet the OS happily resolves the uppercase path to the same file. Affected versions: all @fastify/static < 10.1.4. Fixed in 10.1.4. Impact is read-only (confidentiality of files already within the configured static root); no directory traversal outside the root, no code execution, no write.
The vendor's MEDIUM 5.3 is honest but slightly generous. The CVSS vector AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N correctly captures unauthenticated remote access and low confidentiality impact, but it does not encode the platform prerequisite: the bug is a no-op on Linux (case-sensitive), which is where the overwhelming majority of production Node.js workloads run. Factor in that exploitation also requires the application to actually *use* allowedPath or route guards (without guards, there is nothing to bypass), and the real-world blast radius shrinks further. noisgate adjusts to MEDIUM 4.8 — the narrowing factors don't justify dropping to LOW because the attack is trivially simple when conditions align, but they do pull the score below the vendor's 5.3.
4 steps from start to impact.
Identify a @fastify/static endpoint with route guards
allowedPath or route-level middleware that restricts access to certain paths (e.g., /deep/* requires authentication). This is observable through normal interaction — requesting a guarded path returns 401/403.- Target runs @fastify/static < 10.1.4
- Target uses allowedPath callback or route-level guards on static paths
- Many @fastify/static deployments serve fully public assets and use no guards at all — nothing to bypass
Determine the target OS is case-insensitive
/Favicon.ICO vs /favicon.ico — if both return the same file, the filesystem is case-insensitive. On Linux (ext4, XFS) this returns 404 for the wrong case, and the attack stops here.- Server runs on Windows or macOS with default HFS+ / APFS (case-insensitive)
- ~80-90% of production Node.js servers run Linux (case-sensitive), making this a non-issue for the majority of the fleet
- macOS is primarily used for development, not production hosting
Send case-altered request to bypass route guard
GET /DEEP/secret.txt or GET /Deep/Secret.txt. Fastify's find-my-way router does not match this against the /deep/* route (case-sensitive match), so the request falls through to the static file handler's catch-all. The allowedPath callback, if bound to the guarded route, never executes. The OS resolves the file case-insensitively and @fastify/send serves it.- Previous two conditions met
- The file must exist within the configured static root — no traversal outside it
- Impact is read-only; no write, no RCE, no lateral movement
Exfiltrate guarded static content
- Sensitive files exist within the static root behind the bypassed guard
- Truly sensitive secrets (API keys, database credentials) rarely live in a static-file directory
- If the static root contains only frontend bundles (JS/CSS/images), the bypass yields nothing of value
set $uri_lower $uri; # use lua or map to lowercase.allowedPath callback to compare paths case-insensitively: return requestedPath.toLowerCase().startsWith('/allowed/'). This closes the specific bypass at the application level.- Network-level controls (firewall, segmentation) — the attack is a normal HTTP GET request on the application's legitimate port. No network control can distinguish it from a valid request.
- Authentication middleware on non-static routes — the bypass specifically targets the mismatch between the router's case-sensitive matching and the OS's case-insensitive resolution. Auth on other routes does not protect the static handler's catch-all.
- Content Security Policy / CORS headers — these are response-side controls; they do not prevent the server from resolving and serving the file to the attacker's direct request.
The supporting signals.
| In-the-Wild Exploitation | No evidence of active exploitation. Not on CISA KEV. Disclosed same day as this assessment (2026-09-17). |
|---|---|
| Proof of Concept | Trivial — curl https://target/DEEP/secret.txt on a Windows-hosted Fastify app. No weaponized tooling needed. Credited to 0xRenSec. |
| EPSS Score | Not yet scored — CVE published 2026-09-17, EPSS typically lags 1-2 days. Expect low probability given platform constraint. |
| KEV Status | Not listed. No federal mandate for accelerated patching. |
| CVSS Vector | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N — unauthenticated network access, low confidentiality impact only. No integrity or availability impact. |
| Affected Versions | @fastify/static < 10.1.4 (all prior versions back to at least 8.x). The legacy fastify-static package (pre-scoped) is likely affected but unmaintained. |
| Fixed Version | 10.1.4 — validates requested path spelling against on-disk names, rejecting case-aliased variants before authorization. |
| Bypass Series Context | 6th bypass in the allowedPath/route-guard series since April 2026: CVE-2026-6410, CVE-2026-6414, CVE-2026-7120, CVE-2026-15074, CVE-2026-18427, now CVE-2026-90982. Pattern suggests more variants may follow. |
| Exposure Data | ~852K weekly npm downloads. However, exploitability requires Windows/macOS hosting AND active use of route guards — estimated exposed population is <5% of installs. |
| Disclosure | 2026-09-17 via GitHub Security Advisory GHSA-r799-r9gc-m956. Coordinated disclosure with fix available same day. |
Sources.
Why this verdict
- Platform narrowing: The bug is a complete no-op on Linux (ext4, XFS, Btrfs — all case-sensitive). The vast majority of production Node.js runs on Linux. This single prerequisite eliminates ~85% of the installed base from exposure.
- Guard prerequisite: Exploitation requires the application to use
allowedPathor route-level middleware on static paths. Applications serving fully public static assets have nothing to bypass, further shrinking the vulnerable population. - Read-only, root-scoped impact: The CVSS vector correctly assigns C:L with I:N/A:N. The attacker can only read files already within the configured static root — no traversal beyond it, no code execution, no write capability. Blast radius is a subset of one application's static directory.
- Role multiplier: @fastify/static is a web application library for serving frontend assets. It is not a canonical high-value-role component (not an IdP, hypervisor, DC, backup server, CI/CD, or network edge). In its typical deployment role, a successful exploit yields read access to static files (JS bundles, images, HTML) — blast radius is application-scoped, not fleet-scoped. No high-value role floor applies.
- Trivial exploitability when conditions align: Despite the narrowing factors, the attack requires zero authentication, zero user interaction, and zero sophistication — a single HTTP request. This prevents further downgrade below MEDIUM.
Why not higher?
The bug cannot execute code, write files, or escape the static root. It only affects case-insensitive filesystems, which excludes the dominant Linux production base. The affected component (@fastify/static) is an application-layer library, not infrastructure — even a successful exploit is scoped to one application's static directory, not a domain, fleet, or supply chain. No KEV listing, no active exploitation, no high-value role multiplier.
Why not lower?
The attack is unauthenticated and trivially simple — a single HTTP request with altered casing. When conditions align (Windows server, guarded paths, sensitive files in static root), an attacker gets immediate read access with no tooling. The ongoing bypass series (6 variants in 5 months) signals systemic normalization issues in the codebase, raising the probability that deployments lagging on patches are exposed to multiple compounding bypasses. This is not backlog-tier.
Crowdsourced verification payload.
Run this on your auditor workstation against each target URL. No special privileges needed — it's just HTTP requests. Example: bash check_cve_2026_90982.sh https://myapp.example.com/deep/secret.txt
#!/usr/bin/env bash
# CVE-2026-90982 — @fastify/static case-folding route guard bypass checker
# Usage: ./check_cve_2026_90982.sh <guarded_url>
# e.g. ./check_cve_2026_90982.sh https://app.example.com/deep/secret.txt
# The URL should be a path you KNOW is behind a route guard (returns 401/403).
# No privileges required — runs HTTP requests only.
set -euo pipefail
if [ $# -lt 1 ]; then
echo "Usage: $0 <guarded_url>"
echo " Provide a URL that should return 401/403 when accessed normally."
exit 2
fi
URL="$1"
# Step 1: Confirm the original path is guarded
ORIG_STATUS=$(curl -s -o /dev/null -w '%{http_code}' "$URL")
if [ "$ORIG_STATUS" -lt 400 ]; then
echo "UNKNOWN — Original URL returned HTTP $ORIG_STATUS (expected 401/403). Is this path actually guarded?"
exit 2
fi
echo "[+] Original path returns HTTP $ORIG_STATUS (guarded) — good."
# Step 2: Build an uppercase variant of the path component
# Extract scheme+host and path separately
SCHEME_HOST=$(echo "$URL" | grep -oP '^https?://[^/]+')
PATH_PART=$(echo "$URL" | grep -oP '^https?://[^/]+\K.*')
UPPER_PATH=$(echo "$PATH_PART" | tr '[:lower:]' '[:upper:]')
BYPASS_URL="${SCHEME_HOST}${UPPER_PATH}"
echo "[+] Testing bypass URL: $BYPASS_URL"
BYPASS_STATUS=$(curl -s -o /dev/null -w '%{http_code}' "$BYPASS_URL")
if [ "$BYPASS_STATUS" -ge 200 ] && [ "$BYPASS_STATUS" -lt 300 ]; then
echo "VULNERABLE — Uppercase path returned HTTP $BYPASS_STATUS (bypassed guard). CVE-2026-90982 confirmed."
exit 1
elif [ "$BYPASS_STATUS" -ge 400 ]; then
echo "PATCHED — Uppercase path returned HTTP $BYPASS_STATUS (guard held or case-sensitive FS). Not exploitable."
exit 0
else
echo "UNKNOWN — Uppercase path returned HTTP $BYPASS_STATUS. Manual review recommended."
exit 2
fi