VYPR
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.

PackageAffected versionsPatched versions
@fastify/reply-fromnpm
< 12.5.012.5.0

Affected products

1

Patches

1
4d9795cd5b57

Merge commit from fork

https://github.com/fastify/fastify-reply-fromRoberto BianchiDec 1, 2025via ghsa
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

News mentions

0

No linked articles in our index yet.