Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ function fastifyExpress (fastify, options, next) {
}

const { url } = req.raw

const decodedUrl = FindMyWay.sanitizeUrlPath(url)
// Decode URL before Express matches middleware to prevent encoded path bypass
// e.g., /%61dmin should match middleware registered on /admin
req.raw.url = decodedUrl
req.raw.originalUrl = url
req.raw.id = req.id
req.raw.hostname = req.hostname
Expand All @@ -51,7 +56,7 @@ function fastifyExpress (fastify, options, next) {
reply.raw.log = req.log
reply.raw.send = function send (...args) {
// Restore req.raw.url to its original value https://github.com/fastify/fastify-express/issues/11
req.raw.url = url
req.raw.url = decodedUrl
return reply.send.apply(reply, args)
}

Expand Down Expand Up @@ -81,9 +86,6 @@ function fastifyExpress (fastify, options, next) {
reply.raw.setHeader(headerName, headerValue)
}

// Decode URL before Express matches middleware to prevent encoded path bypass
// e.g., /%61dmin should match middleware registered on /admin
req.raw.url = FindMyWay.sanitizeUrlPath(req.raw.url)
this.express(req.raw, reply.raw, next)
} else {
next()
Expand Down
111 changes: 78 additions & 33 deletions test/middleware.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const { test } = require('node:test')
const fastify = require('fastify')
const fp = require('fastify-plugin')
const cors = require('cors')
const express = require('express')
const helmet = require('helmet')

const expressPlugin = require('../index')
Expand Down Expand Up @@ -367,48 +368,92 @@ test('middlewares should run in the order in which they are defined', async t =>
})

test('middlewares for encoded paths', async t => {
t.plan(2)
await t.test('decode the request url and run the middleware', async (t) => {
await checkEncodedPath('/encoded', '/%65ncoded', t)
})

const instance = fastify()
t.after(() => instance.close())
instance.register(expressPlugin)
.after(() => {
instance.use('/encoded', function (req, _res, next) {
req.slashed = true
next()
})
instance.use('/%65ncoded', function (req, _res, next) {
req.slashedSpecial = true
next()
})
await t.test('does not double decode the url', async (t) => {
await checkEncodedPath('/%65ncoded', '/%2565ncoded', t)
})

await t.test('handle the decoding for express handlers', async (t) => {
t.plan(6)

const routeUrl = '/express'
const requestUrl = '/%65xpress'

const instance = fastify()
t.after(() => instance.close())

instance.addHook('onSend', async function hook (request, reply, payload) {
t.assert.deepStrictEqual(request.raw.url, routeUrl)
t.assert.deepStrictEqual(request.raw.originalUrl, requestUrl)
return payload
})

function handler (request, reply) {
reply.send({
slashed: request.raw.slashed,
slashedSpecial: request.raw.slashedSpecial
instance.addHook('onResponse', async function hook (request, reply) {
t.assert.deepStrictEqual(request.raw.url, routeUrl)
t.assert.deepStrictEqual(request.raw.originalUrl, requestUrl)
})
}

instance.get('/encoded', handler)
instance.get('/%65ncoded', handler)
await instance.register(expressPlugin)

const address = await instance.listen({ port: 0 })
// Register the express-like middleware
instance.use(routeUrl, function (req, _res, next) {
req.slashedByExpress = true
next()
})

await t.test('decode the request url and run the middleware', async (t) => {
t.plan(2)
const response = await fetch(address + '/%65ncod%65d') // '/encoded'
t.assert.ok(response.ok)
const body = await response.json()
t.assert.deepStrictEqual(body, { slashed: true })
})
// Register an express Router with an express handler
const innerRouter = express.Router()
innerRouter.get(routeUrl, function (req, res) {
res.send({ slashedByExpress: req.slashedByExpress })
})
instance.use(innerRouter)

await t.test('does not double decode the url', async (t) => {
t.plan(2)
const response = await fetch(address + '/%2565ncoded')
const body = await response.json()
const address = await instance.listen({ port: 0 })

const response = await fetch(address + requestUrl)
const body = await response.json()
t.assert.ok(response.ok)
t.assert.deepStrictEqual(body, { slashedSpecial: true })
t.assert.deepStrictEqual(body, { slashedByExpress: true })
})
})

async function checkEncodedPath (routeUrl, requestUrl, t) {
t.plan(6)

const instance = fastify()
t.after(() => instance.close())

instance.addHook('onSend', async function hook (request, reply, payload) {
t.assert.deepStrictEqual(request.raw.url, routeUrl)
t.assert.deepStrictEqual(request.raw.originalUrl, requestUrl)
return payload
})

instance.addHook('onResponse', async function hook (request, reply) {
t.assert.deepStrictEqual(request.raw.url, routeUrl)
t.assert.deepStrictEqual(request.raw.originalUrl, requestUrl)
})

await instance.register(expressPlugin)

// Register the express-like middleware
instance.use(routeUrl, function (req, _res, next) {
req.slashed = true
next()
})

// ... with a Fastify route handler
instance.get(routeUrl, (request, reply) => {
reply.send({ slashed: request.raw.slashed, })
})

const address = await instance.listen({ port: 0 })

const response = await fetch(address + requestUrl)
const body = await response.json()
t.assert.ok(response.ok)
t.assert.deepStrictEqual(body, { slashed: true })
}