CVE-2021-21322 (JavaScript, published 2021-03-02T04:15Z)
Description: value: fastify-http-proxy is an npm package which is a fastify plugin for proxying your http requests to another server, with hooks. By crafting a specific URL, it is possible to escape the prefix of the proxied backend service. If the base url of the proxied server is `/pub/`, a user expect that accessing `/priv` on the target service would not be possible. In affected versions, it is possible. This is fixed in version 4.3.1.
Severity: HIGH (CVSS v3 base score: 9.8)
Weakness: CWE-20 \u2014 Improper Input Validation
The product receives input or data, but it does
        not validate or incorrectly validates that the input has the
        properties that are required to process the data safely and
        correctly.
Repository: https://github.com/fastify/fastify-http-proxy
Files changed:
index.js
package.json
test/test.js
test/ws-prefix-rewrite.js
Commit message: Merge pull request from GHSA-c4qr-gmr9-v23w

* added failing test

* fix first test

* Fixed 2/4

* three out of four

* four out of four

Vulnerable code:
function createWebSocketUrl (options, request) {
  const source = new URL(request.url, 'http://127.0.0.1')

  const target = new URL(
    options.rewritePrefix || options.prefix || source.pathname,
    options.upstream
  )

  target.search = source.search

  return target
}

function setupWebSocketProxy (fastify, options) {
    path: options.prefix,
    const url = createWebSocketUrl(options, request)
  const rewritePrefix = opts.rewritePrefix || ''
    setupWebSocketProxy(fastify, opts)
          url: '/*',

Fixed code:
function setupWebSocketProxy (fastify, options, rewritePrefix) {
    if (fastify.prefix && !request.url.startsWith(fastify.prefix)) {
      fastify.log.debug({ url: request.url }, 'not matching prefix')
      source.close()
      return
    }

    const url = createWebSocketUrl(request)

  function createWebSocketUrl (request) {
    const source = new URL(request.url, 'http://127.0.0.1')

    const target = new URL(
      source.pathname.replace(fastify.prefix, rewritePrefix),
      options.upstream
    )

    target.search = source.search

    return target
  }
}

function generateRewritePrefix (prefix, opts) {
  if (!prefix) {
    return ''
  }

  let rewritePrefix = opts.rewritePrefix || new URL(opts.upstream).pathname

  if (!prefix.endsWith('/') && rewritePrefix.endsWith('/')) {
    rewritePrefix = rewritePrefix.slice(0, -1)
  }

  return rewritePrefix
  const rewritePrefix = generateRewritePrefix(fastify.prefix, opts)
    setupWebSocketProxy(fastify, opts, rewritePrefix)
    "fastify-websocket": "^3.0.0",
          url: '/',
'use strict'

const t = require('tap')
const { once } = require('events')

const Fastify = require('fastify')
const fastifyWebSocket = require('fastify-websocket')
const proxy = require('..')
const WebSocket = require('ws')
const got = require('got')

const level = 'warn'

async function proxyServer (t, backendURL, backendPath, proxyOptions, wrapperOptions) {
  const frontend = Fastify({ logger: { level } })
  const registerProxy = async fastify => {
    fastify.register(proxy, {
      upstream: backendURL + backendPath,
      ...proxyOptions
    })
  }

  t.comment('starting proxy to ' + backendURL + backendPath)

  if (wrapperOptions) {
    await frontend.register(registerProxy, wrapperOptions)
  } else {
    await registerProxy(frontend)
  }

  return [frontend, await frontend.listen(0)]
}

async function processRequest (t, frontendURL, path, expected) {
  const url = new URL(path, frontendURL)
  t.comment('ws connecting to ' + url.toString())
  const ws = new WebSocket(url)
  let wsResult, gotResult

  try {
    await once(ws, 'open')
    t.pass('socket connected')

    const [buf] = await Promise.race([once(ws, 'message'), once(ws, 'close')])
    if (buf instanceof Buffer) {
      wsResult = buf.toString()
    } else {
      t.comment('websocket closed')
      wsResult = 'error'
    }
  } catch (e) {
    wsResult = 'error'
    ws.terminate()
  }

  try {
    const result = await got(url)
    gotResult = result.body
  } catch (e) {
    gotResult = 'error'
  }

  t.is(wsResult, expected)
  t.is(gotResult, expected)
}

async function handleProxy (info, { backendPath, proxyOptions, wrapperOptions }, expected, ...paths) {
  t.test(info, async function (t) {
    const backend = Fastify({ logger: { level } })
    await backend.register(fastifyWebSocket)

    backend.get('/*', {
      handler: (req, reply) => {
        reply.send(req.url)
      },
      wsHandler: (conn, req) => {
        conn.write(req.url)
        conn.end()
      }
    })

    t.teardown(() => backend.close())

    const backendURL = await backend.listen(0)

    const [frontend, frontendURL] = await proxyServer(t, backendURL, backendPath, proxyOptions, wrapperOptions)

    t.teardown(() => frontend.close())

    for (const path of paths) {
      await processRequest(t, frontendURL, path, expected(path))
    }

    t.end()
  })
}

handleProxy(
  'no prefix to `/`',
  {
    backendPath: '/',
    proxyOptions: { websocket: true }
  },
  path => path,
  '/',
  '/pub',
  '/pub/'
)

handleProxy(
  '`/pub/` to `/`',
  {
    backendPath: '/',
    proxyOptions: { websocket: true, prefix: '/pub/' }
  },
  path => path.startsWith('/pub/') ? path.replace('/pub/', '/') : 'error',
  '/',
  '/pub/',
  '/pub/test'
)

handleProxy(
  '`/pub/` to `/public/`',
  {
    backendPath: '/public/',
    proxyOptions: { websocket: true, prefix: '/pub/' }
  },
  path => path.startsWith('/pub/') ? path.replace('/pub/', '/public/') : 'error',
  '/',
  '/pub/',
  '/pub/test'
)

handleProxy(
  'wrapped `/pub/` to `/public/`',
  {
    backendPath: '/public/',
    proxyOptions: { websocket: true },
    wrapperOptions: { prefix: '/pub/' }
  },
  path => path.startsWith('/pub/') ? path.replace('/pub/', '/public/') : 'error',
  '/',
  '/pub/',
  '/pub/test'
)