Repository navigation
Commit 36ff5d8
fix(api): stop /api/grade from fetching arbitrary internal URLs
`/api/grade?url=` handed a caller-supplied string straight to `fetch()`: no
scheme check, no address check, no timeout and no size cap, on a route with
`maxDuration: 300`. Anything the function could reach, a visitor could reach
through it — `169.254.169.254` included — and read back as a 400 or a grade.
The multipart branch had the matching problem: with `bodyParser: false`
nothing bounded the stream it buffered.
The feature is "paste a link to your CV", so a host allowlist isn't on the
table. `fetchRemoteResume` guards where the connection actually lands:
- https only, no embedded credentials, port 443 only — an arbitrary port
turns this into an internal port scanner.
- Every resolved address is checked against loopback, RFC1918, CGNAT,
link-local, benchmarking, documentation, multicast and reserved space, v4
and v6, including the transition formats that hide a v4 address inside a v6
one. If any record for a host is private the host is refused, so a
round-robin that mixes public and private answers doesn't get a second roll.
- The socket is pinned to the address we validated, with `servername` and
`Host` still set to the name, so a DNS rebind can't swap the metadata
endpoint in between the check and the connect. Validate-then-`fetch()`
leaves that window open.
- Redirects are followed by hand, three at most, each hop re-parsed and
re-resolved from scratch — a `Location` is a fresh caller-supplied URL.
- 20s deadline across all hops, and a 10MB cap on both `content-length` and
the running total, since a chunked response declares nothing.
Failures carry stable codes, so the route answers 4xx instead of falling
through to the 500, and `getErrorMessage` maps them to copy. It now reads the
code before looking at the status: our own 413 knows the route's limit, while
Vercel's plain-text 413 is still the one that mentions its 4.5MB payload cap.
Tests are network-free: address classification both ways, and the URL shapes
refused before a socket opens — decimal and hex loopback encodings among them,
which `new URL()` normalizes and the resolver would otherwise reach happily.
The timeout and both size-cap branches were checked against real origins
locally; those cases stay out of the suite rather than make CI depend on
third-party hosts.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xa76D7n4CLkdKt99Su1HRy1 parent 9013deb commit 36ff5d8
6 files changed
Lines changed: 552 additions & 7 deletions
File tree
- src
- pages/api
- resume-checker
- tests
- api
- resume-checker
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
1 | 6 | | |
2 | 7 | | |
3 | 8 | | |
| |||
42 | 47 | | |
43 | 48 | | |
44 | 49 | | |
45 | | - | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
46 | 54 | | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
47 | 61 | | |
48 | 62 | | |
49 | 63 | | |
| |||
54 | 68 | | |
55 | 69 | | |
56 | 70 | | |
57 | | - | |
58 | | - | |
59 | | - | |
| 71 | + | |
60 | 72 | | |
61 | 73 | | |
62 | 74 | | |
| |||
84 | 96 | | |
85 | 97 | | |
86 | 98 | | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
87 | 107 | | |
88 | 108 | | |
89 | 109 | | |
| |||
0 commit comments