CVE-2026-90982
@fastify/static — Route-guard bypass via path case-folding
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:NSummary
@fastify/static serves files from a configured root directory. On a case-insensitive filesystem, a route guard or allowedPath restriction can be bypassed by changing the letter case of a path segment.
Technical analysis
The guard compares the request path case-sensitively, but the underlying filesystem on Windows and the default macOS volume resolves paths case-insensitively. A request for a path that differs only in case from a blocked one misses the guarded route, falls through to the static handler, and opens the same file. It is a case-insensitive-filesystem variant of the earlier CVE-2026-18427.
The 10.1.3 canonicalizer ignores case. isNonCanonicalPathname rejects \ and any path that posix.normalize would rewrite. /DEEP/secret.txt is already “normalized”, so it is treated as canonical and allowed through. @fastify/static 10.1.3 — isNonCanonicalPathname
The router is case-sensitive; the disk is not. A prefix hook on /deep/* never sees /DEEP/secret.txt. The static /* handler does. On Windows / default macOS, that request opens the same inode as /deep/secret.txt. find-my-way route match vs @fastify/send
The library already owns this quirk for \. The same helper rejects backslash because send treats it as a separator on Windows. Letter-case is the other Windows/macOS filesystem alias in the same function, left unhandled. That is why this is a library fix, not “the OS did it.” CVE-2026-18427 residual — case axis
function isNonCanonicalPathname(p) {
if (p.includes('\\')) return true // Windows separator — they did fix this
return path.posix.normalize(p) !== p // '/DEEP/secret.txt' === itself → allowed
}
// find-my-way: /DEEP/* does not match /deep/* (case-sensitive)
// send + NTFS/APFS: DEEP/secret.txt opens deep/secret.txtProof of concept
Renesoft confirmed this against the real package in an isolated environment, with no live systems. The attack path, then the recorded test output:
- App mounts static files and a prefix guard — a hook on
/deep/*, or anallowedPathpredicate that denies/secret/. - Request the same path with a different letter case —
GET /DEEP/secret.txtorGET /files/SECRET/data.txt. Encoded dots, extra slashes, and%2Fstay blocked by the earlier CVEs. - The guard never runs; the file is served — response body hashes equal the on-disk guarded file. Changing only the filename case under a still-matching prefix (
/deep/Secret.txt) does not bypass — the guarded segment itself has to fold.
$ node poc/poc.js
GET /deep/secret.txt 401 guard runs
GET /x/../deep/secret.txt 403 CVE-2026-15074 still on
GET //deep/secret.txt 403 CVE-2026-18427 still on
GET /DEEP/secret.txt 200 serves guarded file (sha256 match)
GET /files/secret/data.txt 404 allowedPath denies
GET /files/SECRET/data.txt 200 serves denied file (sha256 match)Impact
On case-insensitive hosts, an attacker reaches files that a path restriction was meant to block.
Affected versions
- Affected
- < 10.1.4
- Fixed
- 10.1.4
- CVSS
- 5.3 (3.1)
- Weakness
- CWE-178 · CWE-284
- Ecosystem
- JavaScript · npm
- Disclosed
- 2026-09-17
Remediation
Fixed in 10.1.4. Applications below that version should upgrade.
Timeline
- Found and reproduced on 10.1.3 as a residual of CVE-2026-18427 on the case axis.
- Reported privately through GitHub private vulnerability reporting.
- Published; fixed in 10.1.4 and CVE-2026-90982 assigned.