CVE
CVE-2026-85184
CWE
CWE-436
Affected Surface
- npm consumers of `@fastify/middie >= 9.1.0 < 9.3.4`
- Fastify applications that use path-scoped middleware like `app.use('/private', auth)` for authentication, authorization, rate limiting, or audit gates
- Frameworks and internal platforms that wrap Fastify plus `@fastify/middie` and trust middleware prefix matching as a security boundary
CVE-2026-85184 is a small parser mismatch with a large security consequence. @fastify/middie exists to let Fastify applications mount Express-style middleware with calls like app.use('/private', authMiddleware). In a lot of codebases, that middleware is the auth layer. If the middleware does not run, the request reaches the route handler without any check in front of it.
The new bug, published on 4 September 2026, lives in how middie decided whether a path-scoped middleware should run. Before 9.3.4, it matched middleware paths against the raw request target. Fastify’s router, via find-my-way, first canonicalized an absolute-form target into its path before dispatching. Those two layers were making a security decision on different strings.
Where the split happens
The relevant setup code is normal for middie. A scoped middleware path is compiled with path-to-regexp and stored as a regular expression:
if (url) {
const pathRegExp = pathToRegexp(sanitizePrefixUrl(url), {
end: false
})
regexp = pathRegExp.regexp
}
At request time, the middleware engine normalizes the URL and tests the regular expression:
const sanitized = sanitizeUrl(req.url)
const normalized = normalizePathForMatching(sanitized, normalizationOptions)
holder.normalizedUrl = normalized.path
const result = regexp.exec(normalizedUrl)
Before the patch, normalizePathForMatching() started with the raw url, optionally collapsed duplicate slashes, and passed the result through safeDecodeURI(). What it did not do was strip scheme and authority from an absolute-form request target such as http://evil.example/private/secrets.
That matters because Fastify’s router does understand absolute-form targets. If the router dispatches /private/secrets but middie still tries to match http://evil.example/private/secrets, then app.use('/private', auth) never fires even though the protected handler still runs.
The shipped regression test shows the bypass clearly
The new security-absolute-form-bypass.test.js file added in 9.3.4 is useful because it encodes the exploit as a real network request instead of a synthetic unit test. It opens a raw TCP socket and sends the request target exactly as the server receives it:
socket.end(`GET ${target} HTTP/1.1\r\nHost: example.test\r\nConnection: close\r\n\r\n`)
The protected application in that test is minimal:
await app.register(middiePlugin)
app.use('/private', blockRequest)
app.get('/private/secrets', async () => ({ secret: true }))
And the exploit targets are exactly the values defenders should search for in edge logs and repro attempts:
const targets = [
'/private/secrets',
'http://evil.example/private/secrets',
'https://evil.example/private/secrets',
'HtTp://evil.example/private/secrets',
'http://user:password@evil.example:8080/private/secrets',
'http://evil.example/private/secrets?source=absolute'
]
In the fixed build, every one of those requests returns 401 Unauthorized. In the vulnerable range, the absolute-form variants are the dangerous ones because they can miss the middleware match while still reaching the route.
What changed in 9.3.4
The patch is short and worth reading. normalizePathForMatching() now checks whether the URL starts with /. If it does not, middie treats it as a possible absolute-form target and resolves the path first:
if (path.charCodeAt(0) !== 47) {
const absolutePath = getPathFromAbsoluteUrl(path)
if (absolutePath === null) {
return { path: url, error: FST_ERR_MIDDIE_MALFORMED_URL() }
}
path = absolutePath
}
The helper added by the patch mirrors find-my-way closely enough to close the drift:
function getPathFromAbsoluteUrl (url) {
const schemeEnd = url.indexOf('://')
if (schemeEnd === -1) return url
const scheme = url.slice(0, schemeEnd).toLowerCase()
if (scheme !== 'http' && scheme !== 'https') return url
const authorityStart = schemeEnd + 3
const pathStart = url.indexOf('/', authorityStart)
if (pathStart === authorityStart) return null
if (!URL.canParse(url)) return null
return pathStart === -1 ? '/' : url.slice(pathStart)
}
There are two important details in that helper:
- malformed absolute-form targets such as
http:///adminnow fail closed withFST_ERR_MIDDIE_MALFORMED_URL(); - valid absolute-form targets without an explicit path normalize to
/, which prevents ambiguous edge behavior.
That is a much safer posture than asking the middleware matcher to reason about raw authority-bearing URLs.
Why this matters beyond one plugin
This is not the first path-canonicalization bug in @fastify/middie this year. Earlier advisories covered encoded slashes, duplicate slash handling, and child-scope inheritance mistakes. The pattern is familiar by now: middie makes one decision about which middleware applies, while Fastify’s router makes another decision about which handler runs.
That repeated theme should change how teams treat this package. @fastify/middie is convenient, but path-scoped middleware in front of sensitive routes is security-critical code. Any mismatch between middleware matching and router matching is an auth bypass until proven otherwise.
Scoping exposure
The directly affected versions are 9.1.0 through 9.3.3. The 9.1.0 boundary matters because the affected range is not “all historical middie versions.” It is the modern branch most Fastify 5 users are likely to pull today.
The highest-risk patterns look like this:
app.use('/admin', authMiddleware)
app.use('/private', rateLimitMiddleware)
app.use('/user/:id/comments', auditMiddleware)
Those patterns are common in:
- migration code that ports Express middleware into Fastify,
- internal control planes and admin surfaces,
- multi-tenant APIs with route-prefix auth,
- platform wrappers that register shared middleware in child scopes.
If your security check lives only in app.use('/prefix', ...), the middleware engine is part of your attack surface.
What to check in a real codebase
Start with the resolved dependency version, not just the declared semver range:
npm ls @fastify/middie
pnpm why @fastify/middie
yarn why @fastify/middie
Then find the places where path-scoped middleware is being used as a guard:
rg -n "app\\.use\\(|fastify\\.use\\(" src app packages .
rg -n "register\\(require\\('@fastify/middie'\\)|register\\(middie" src app packages .
If you find auth, authorization, rate limiting, or auditing attached this way, verify whether the same control also exists in a Fastify-native hook such as onRequest, preValidation, or preHandler.
Remediation
- Upgrade to
@fastify/middie >= 9.3.4. - Rebuild lockfiles and deployment artifacts so the patched version is actually what ships.
- Move high-value auth checks out of path-scoped middleware and into Fastify-native hooks when possible.
- Add regression requests that use absolute-form targets, not only normal origin-form paths.
The key lesson from CVE-2026-85184 is not just that one npm package had a bug. It is that URL canonicalization drift between framework layers keeps turning convenience middleware into a security boundary. If your router and your middleware do not normalize the same request target in the same way, the attacker gets to choose which interpretation wins.
From research to remediation
Check whether this pattern exists in your codebase
Turn this research into a remediation workflow. Scan dependencies and package manifests for similar supply-chain risk, then prioritize fixes with reachability context.