CVE-2026-22037
Description
The @fastify/express plugin adds full Express compatibility to Fastify. A security vulnerability exists in @fastify/express prior to version 4.0.3 where middleware registered with a specific path prefix can be bypassed using URL-encoded characters (e.g., /%61dmin instead of /admin). While the middleware engine fails to match the encoded path and skips execution, the underlying Fastify router correctly decodes the path and matches the route handler, allowing attackers to access protected endpoints without the middleware constraints. The vulnerability is caused by how @fastify/express matches requests against registered middleware paths. This vulnerability is similar to, but differs from, CVE-2026-22031 because this is a different npm module with its own code. Version 4.0.3 of @fastify/express contains a patch fort the issue.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
@fastify/expressnpm | < 4.0.3 | 4.0.3 |
Affected products
1- Range: v0.1.0, v0.2.0, v0.3.0, …
Patches
1dc02a3fe1387fix: decode paths before matching (#174)
3 files changed · +101 −2
index.js+7 −1 modified@@ -2,6 +2,7 @@ const fp = require('fastify-plugin') const Express = require('express') +const FindMyWay = require('find-my-way') const kMiddlewares = Symbol('fastify-express-middlewares') function fastifyExpress (fastify, options, next) { @@ -41,6 +42,11 @@ function fastifyExpress (fastify, options, next) { } const { url } = req.raw + + const decodedUrl = FindMyWay.sanitizeUrlPath(url) + // Decode URL before Express matches middleware to prevent encoded path bypass + // e.g., /%61dmin should match middleware registered on /admin + req.raw.url = decodedUrl req.raw.originalUrl = url req.raw.id = req.id req.raw.hostname = req.hostname @@ -50,7 +56,7 @@ function fastifyExpress (fastify, options, next) { reply.raw.log = req.log reply.raw.send = function send (...args) { // Restore req.raw.url to its original value https://github.com/fastify/fastify-express/issues/11 - req.raw.url = url + req.raw.url = decodedUrl return reply.send.apply(reply, args) }
package.json+2 −1 modified@@ -76,7 +76,8 @@ }, "dependencies": { "express": "^5.1.0", - "fastify-plugin": "^5.0.0" + "fastify-plugin": "^5.0.0", + "find-my-way": "^9.4.0" }, "tsd": { "directory": "test/types"
test/middleware.test.js+92 −0 modified@@ -6,6 +6,7 @@ const { test } = require('node:test') const fastify = require('fastify') const fp = require('fastify-plugin') const cors = require('cors') +const express = require('express') const helmet = require('helmet') const expressPlugin = require('../index') @@ -365,3 +366,94 @@ test('middlewares should run in the order in which they are defined', async t => t.assert.deepStrictEqual(result.status, 200) t.assert.deepStrictEqual(await result.json(), { hello: 'world' }) }) + +test('middlewares for encoded paths', async t => { + await t.test('decode the request url and run the middleware', async (t) => { + await checkEncodedPath('/encoded', '/%65ncoded', t) + }) + + await t.test('does not double decode the url', async (t) => { + await checkEncodedPath('/%65ncoded', '/%2565ncoded', t) + }) + + await t.test('handle the decoding for express handlers', async (t) => { + t.plan(6) + + const routeUrl = '/express' + const requestUrl = '/%65xpress' + + const instance = fastify() + t.after(() => instance.close()) + + instance.addHook('onSend', async function hook (request, reply, payload) { + t.assert.deepStrictEqual(request.raw.url, routeUrl) + t.assert.deepStrictEqual(request.raw.originalUrl, requestUrl) + return payload + }) + + instance.addHook('onResponse', async function hook (request, reply) { + t.assert.deepStrictEqual(request.raw.url, routeUrl) + t.assert.deepStrictEqual(request.raw.originalUrl, requestUrl) + }) + + await instance.register(expressPlugin) + + // Register the express-like middleware + instance.use(routeUrl, function (req, _res, next) { + req.slashedByExpress = true + next() + }) + + // Register an express Router with an express handler + const innerRouter = express.Router() + innerRouter.get(routeUrl, function (req, res) { + res.send({ slashedByExpress: req.slashedByExpress }) + }) + instance.use(innerRouter) + + const address = await instance.listen({ port: 0 }) + + const response = await fetch(address + requestUrl) + const body = await response.json() + t.assert.ok(response.ok) + t.assert.deepStrictEqual(body, { slashedByExpress: true }) + }) +}) + +async function checkEncodedPath (routeUrl, requestUrl, t) { + t.plan(6) + + const instance = fastify() + t.after(() => instance.close()) + + instance.addHook('onSend', async function hook (request, reply, payload) { + t.assert.deepStrictEqual(request.raw.url, routeUrl) + t.assert.deepStrictEqual(request.raw.originalUrl, requestUrl) + return payload + }) + + instance.addHook('onResponse', async function hook (request, reply) { + t.assert.deepStrictEqual(request.raw.url, routeUrl) + t.assert.deepStrictEqual(request.raw.originalUrl, requestUrl) + }) + + await instance.register(expressPlugin) + + // Register the express-like middleware + instance.use(routeUrl, function (req, _res, next) { + req.slashed = true + next() + }) + + // ... with a Fastify route handler + instance.get(routeUrl, (request, reply) => { + reply.send({ slashed: request.raw.slashed, }) + }) + + const address = await instance.listen({ port: 0 }) + + const response = await fetch(address + requestUrl) + const body = await response.json() + t.assert.ok(response.ok) + t.assert.deepStrictEqual(body, { slashed: true }) +}
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-g6q3-96cp-5r5mghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-22037ghsaADVISORY
- github.com/fastify/fastify-express/commit/dc02a3fe1387f945143f22597baa42557d549a40nvdWEB
- github.com/fastify/fastify-express/releases/tag/v4.0.3ghsaWEB
- github.com/fastify/fastify-express/security/advisories/GHSA-g6q3-96cp-5r5mnvdWEB
News mentions
0No linked articles in our index yet.