File size: 3,502 Bytes
837c968
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
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)
  })
})