Moderate severityNVD Advisory· Published Dec 1, 2025· Updated Dec 2, 2025
fastify-reply-from bypass of reply forwarding
CVE-2025-66415
Description
fastify-reply-from is a Fastify plugin to forward the current HTTP request to another server. Prior to 12.5.0, by crafting a malicious URL, an attacker could access routes that are not allowed, even though the reply.from is defined for specific routes in @fastify/reply-from. This vulnerability is fixed in 12.5.0.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
@fastify/reply-fromnpm | < 12.5.0 | 12.5.0 |
Affected products
1- Range: < 12.5.0
Patches
14d9795cd5b57Merge commit from fork
5 files changed · +128 −1
index.js+3 −1 modified@@ -71,7 +71,9 @@ const fastifyReplyFrom = fp(function from (fastify, opts, next) { const retryDelay = opts.retryDelay || undefined if (!source) { - source = req.url + const requestUrl = req.url + const queryIndex = requestUrl.indexOf('?') + source = queryIndex >= 0 ? requestUrl.substring(0, queryIndex) : requestUrl } // we leverage caching to avoid parsing the destination URL
lib/utils.js+6 −0 modified@@ -63,6 +63,12 @@ function stripHttp1ConnectionHeaders (headers) { // issue ref: https://github.com/fastify/fast-proxy/issues/42 function buildURL (source, reqBase) { + if (decodeURIComponent(source).includes('..')) { + const err = new Error('source/request contain invalid characters') + err.statusCode = 400 + throw err + } + if (Array.isArray(reqBase)) reqBase = reqBase[0] let baseOrigin = reqBase ? new URL(reqBase).href : undefined
test/build-url.test.js+32 −0 modified@@ -76,3 +76,35 @@ test('should throw when trying to override base', async (t) => { await Promise.all(promises) }) + +test('should throw on path traversal attempts', (t) => { + t.assert.throws( + () => buildURL('/foo/bar/../', 'http://localhost'), + new Error('source/request contain invalid characters') + ) + + t.assert.throws( + () => buildURL('/foo/bar/..', 'http://localhost'), + new Error('source/request contain invalid characters') + ) + + t.assert.throws( + () => buildURL('/foo/bar/%2e%2e/', 'http://localhost'), + new Error('source/request contain invalid characters') + ) + + t.assert.throws( + () => buildURL('/foo/bar/%2E%2E/', 'http://localhost'), + new Error('source/request contain invalid characters') + ) + + t.assert.throws( + () => buildURL('/foo/bar/..%2f', 'http://localhost'), + new Error('source/request contain invalid characters') + ) + + t.assert.throws( + () => buildURL('/foo/bar/%2e%2e%2f', 'http://localhost'), + new Error('source/request contain invalid characters') + ) +})
test/fix-GHSA-2q7r-29rg-6m5h.test.js+32 −0 added@@ -0,0 +1,32 @@ +'use strict' + +const t = require('node:test') +const Fastify = require('fastify') +const { request } = require('undici') +const From = require('..') +const http = require('node:http') + +const instance = Fastify() + +t.test('fix for GHSA-2q7r-29rg-6m5h vulnerability', async (t) => { + t.plan(2) + + const target = http.createServer((_, res) => { + res.statusCode = 205 + res.end('hi') + }) + await target.listen({ port: 0 }) + t.after(() => target.close()) + + instance.get('/', (_request, reply) => { reply.from('/ho/%2E%2E/hi') }) + instance.register(From, { + base: `http://localhost:${target.address().port}/hi/`, + undici: true + }) + await instance.listen({ port: 0 }) + t.after(() => instance.close()) + + const { statusCode, body } = await request(`http://localhost:${instance.server.address().port}`) + t.assert.strictEqual(statusCode, 400) + t.assert.strictEqual(await body.text(), '{"statusCode":400,"error":"Bad Request","message":"source/request contain invalid characters"}') +})
test/full-querystring-url.test.js+55 −0 added@@ -0,0 +1,55 @@ +'use strict' + +const t = require('node:test') +const Fastify = require('fastify') +const { request } = require('undici') +const From = require('..') +const http = require('node:http') + +const instance = Fastify() + +t.test('full querystring url', async (t) => { + const target = http.createServer((req, res) => { + t.assert.ok('request proxied') + t.assert.strictEqual(req.method, 'GET') + t.assert.strictEqual(req.url, '/hi?a=/ho/%2E%2E/hi') + res.statusCode = 205 + res.setHeader('Content-Type', 'text/plain') + res.setHeader('x-my-header', 'hi!') + res.end('hi') + }) + + await target.listen({ port: 0 }) + t.after(() => target.close()) + + await instance.register(From, { + base: `http://localhost:${target.address().port}` + }) + + instance.get('/hi', (_request, reply) => { + reply.from() + }) + + instance.get('/foo', (_request, reply) => { + reply.from('/hi') + }) + + await instance.listen({ port: 0 }) + t.after(() => instance.close()) + + { + const result = await request(`http://localhost:${instance.server.address().port}/hi?a=/ho/%2E%2E/hi`) + t.assert.strictEqual(result.headers['content-type'], 'text/plain') + t.assert.strictEqual(result.headers['x-my-header'], 'hi!') + t.assert.strictEqual(result.statusCode, 205) + t.assert.strictEqual(await result.body.text(), 'hi') + } + + { + const result = await request(`http://localhost:${instance.server.address().port}/foo?a=/ho/%2E%2E/hi`) + t.assert.strictEqual(result.headers['content-type'], 'text/plain') + t.assert.strictEqual(result.headers['x-my-header'], 'hi!') + t.assert.strictEqual(result.statusCode, 205) + t.assert.strictEqual(await result.body.text(), 'hi') + } +})
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
4- github.com/advisories/GHSA-2q7r-29rg-6m5hghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-66415ghsaADVISORY
- github.com/fastify/fastify-reply-from/commit/4d9795cd5b57a36756d37b7f036eae369f69fa66ghsax_refsource_MISCWEB
- github.com/fastify/fastify-reply-from/security/advisories/GHSA-2q7r-29rg-6m5hghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.