Gateprep / frontend /api /proxy.test.js
banu4prasad's picture
SSRF proxy fixes
837c968
Raw
History Blame Contribute Delete
3.5 kB
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
import { buildTargetUrl } from './proxy.js'
const BACKEND = 'https://real-backend.hf.space'
/**
* Build a minimal mock request object that mirrors what Vercel passes to
* serverless handlers β€” just enough for buildTargetUrl to work.
*/
function fakeReq(path) {
return { url: `/api/proxy?path=${encodeURIComponent(path)}` }
}
beforeAll(() => {
process.env.API_PROXY_TARGET = BACKEND
})
afterAll(() => {
delete process.env.API_PROXY_TARGET
})
// ── Payloads that MUST be allowed (legitimate routes) ────────────
describe('buildTargetUrl β€” allowed paths', () => {
const allowed = [
['auth/login', `${BACKEND}/auth/login`],
['tests/5/results', `${BACKEND}/tests/5/results`],
// double-slash is treated as a same-origin relative path
['//attacker.example.com/steal', `${BACKEND}/attacker.example.com/steal`],
// bare hostname without scheme is a normal path segment
['evil.com/steal', `${BACKEND}/evil.com/steal`],
// hostname with userinfo-style @ is just a path segment
['real-backend.hf.space@attacker.example.com/steal', `${BACKEND}/real-backend.hf.space@attacker.example.com/steal`],
// ISO timestamp with colon should not false-positive
['2026-07-15T10:00:00/results', `${BACKEND}/2026-07-15T10:00:00/results`],
]
it.each(allowed)('allows "%s" β†’ %s', (path, expectedHref) => {
const target = buildTargetUrl(fakeReq(path))
expect(target.href).toBe(expectedHref)
})
})
// ── Payloads that MUST be blocked (SSRF attempts) ────────────────
describe('buildTargetUrl β€” blocked paths', () => {
const blocked = [
'http://attacker.example.com/steal',
'HTTP://attacker.example.com/steal',
'https://attacker.example.com/steal',
'javascript:alert(1)',
'data:text/html,evil',
'ftp://attacker.example.com/exfil',
// double backslash β€” WHATWG URL parser treats \\ as // for special
// schemes, so this resolves as a network-path reference with
// attacker.example.com as the host. Layer 1 (regex) does not catch
// this; layer 2 (origin check) does.
'\\\\attacker.example.com/steal',
// null-byte / control-character smuggling attempts
'ht\ttp://attacker.example.com/steal',
]
it.each(blocked)('blocks "%s"', (path) => {
expect(() => buildTargetUrl(fakeReq(path))).toThrow('Invalid proxy path')
})
})
// ── Edge cases ───────────────────────────────────────────────────
describe('buildTargetUrl β€” edge cases', () => {
it('rejects when API_PROXY_TARGET is not set', () => {
const saved = process.env.API_PROXY_TARGET
delete process.env.API_PROXY_TARGET
delete process.env.BACKEND_URL
try {
expect(() => buildTargetUrl(fakeReq('auth/login'))).toThrow(
'API_PROXY_TARGET is not configured',
)
} finally {
process.env.API_PROXY_TARGET = saved
}
})
it('passes through extra query params', () => {
const req = { url: '/api/proxy?path=tests&page=2&limit=10' }
const target = buildTargetUrl(req)
expect(target.searchParams.get('page')).toBe('2')
expect(target.searchParams.get('limit')).toBe('10')
})
it('handles empty path gracefully', () => {
const target = buildTargetUrl(fakeReq(''))
expect(target.origin).toBe(new URL(BACKEND).origin)
})
})