Commit b510d2a
authored
fix(serverless): block SSRF in job-input downloads (#533)
* fix(serverless): block SSRF in job-input downloads
Job input carries arbitrary URLs that the worker downloads. Without
restriction a job can point the worker at cloud instance-metadata
(169.254.169.254) or other non-public addresses (CWE-918).
Add runpod/serverless/utils/rp_ssrf.py: an http(s) scheme allowlist,
DNS resolution that fails closed on any non-global IP (link-local,
loopback, RFC1918, CGNAT, reserved, multicast, IPv6 ULA), a connection
adapter that pins the socket to the validated IP to defeat DNS
rebinding, per-hop redirect re-validation, and a streamed size cap.
Route both rp_download fetch sites through it, and stream file() to disk
instead of buffering the whole untrusted body in memory.
Blocks raise SSRFError (a ValueError, not a RequestException) so they
fail loudly rather than being retried or silently dropped. Configurable
via RUNPOD_ALLOW_PRIVATE_DOWNLOAD_URLS (escape hatch, off by default) and
RUNPOD_MAX_DOWNLOAD_BYTES (default 5 GiB).
* fix(serverless): close pinned SSRF sessions to prevent FD leaks
Address PR #533 review feedback.
- Tie each single-use SyncClientSession's lifecycle to its response via
_bind_session_to_response, so closing the response (with-block or explicit)
also closes the session; without it the session's pools/FDs leaked once per
download in long-lived workers.
- Close the session on redirect hops and on request exceptions; make teardown
best-effort so a cleanup failure cannot mask the caller's real error.
- Close response and session deterministically in the pinned-IP adapter test.
- Add tests for the request-exception path and the error-masking guarantee.
* fix(serverless): tolerate malformed Content-Length in downloads
Content-Length is remote-controlled, so int() on it can raise ValueError
and abort an otherwise valid download. In file() this was a new failure
path (main never parsed the header there); in download_files_from_urls
the ValueError also escaped both the backoff retry and the RequestException
handler, failing the whole batch.
Parse defensively via parse_content_length(), falling back to 0 so chunk
sizing still works. The size limit is unaffected: it is enforced while
streaming by iter_content_capped().
* fix(serverless): fall back across validated IPs when pinning
Pinning to ips[0] turned a multi-address host into a hard failure when the
first address is unroutable from the worker: a dual-stack host whose AAAA
sorts first is unreachable on a pod with no IPv6 route, where plain requests
would have tried the A record. Walk the validated addresses in resolution
order, advancing only on connection errors, so every candidate stays
pre-validated while the fallback behavior is preserved.
Also patch os.makedirs in the file() tests, which were creating a real
job_files/ directory in the working tree.
* fix(serverless): fail file() on HTTP error responses
file() saved whatever body came back, so a 4xx/5xx error page was written
as the downloaded file and, when the URL ended in .zip, handed to the
extractor. download_files_from_urls already called raise_for_status();
file() never did.
Behavior change: file() now raises requests.RequestException on an error
status instead of returning a dict pointing at the error page. It has no
in-tree callers and is not re-exported from serverless.utils, so this is
limited to direct importers.
* fix(serverless): bracket IPv6 literals in the pinned Host header
urlparse strips the brackets from an IPv6-literal URL host, so the adapter
built a malformed Host header ("2606::1:443" instead of "[2606::1]:443"),
which RFC 7230 requires and some servers reject. Extracted the authority
construction into _host_header_for() and covered every host form.
* fix(serverless): refuse proxied fetches that defeat IP pinning
Pinning only holds when this process opens the socket. Through a proxy the
request carries the hostname and the proxy resolves it, so the validated IP
is not what gets dialed and the DNS-rebind protection silently stops
applying while still appearing to be in force.
Refuse such a URL with SSRFError instead. NO_PROXY exclusions are honored,
since those hosts are fetched directly, and RUNPOD_ALLOW_PRIVATE_DOWNLOAD_URLS
allows the proxied fetch for deployments that accept the trade-off. Chosen
over trust_env=False, which would silently ignore a needed proxy and also
drop REQUESTS_CA_BUNDLE.1 parent bc9f6e1 commit b510d2a
4 files changed
Lines changed: 995 additions & 100 deletions
File tree
- runpod/serverless/utils
- tests/test_serverless/test_utils
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
16 | 16 | | |
17 | 17 | | |
18 | 18 | | |
19 | | - | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
20 | 24 | | |
21 | 25 | | |
22 | 26 | | |
| |||
33 | 37 | | |
34 | 38 | | |
35 | 39 | | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
36 | 54 | | |
37 | 55 | | |
38 | 56 | | |
| |||
57 | 75 | | |
58 | 76 | | |
59 | 77 | | |
60 | | - | |
| 78 | + | |
61 | 79 | | |
62 | 80 | | |
63 | 81 | | |
| |||
69 | 87 | | |
70 | 88 | | |
71 | 89 | | |
72 | | - | |
73 | | - | |
| 90 | + | |
74 | 91 | | |
75 | | - | |
| 92 | + | |
76 | 93 | | |
77 | | - | |
78 | | - | |
79 | | - | |
| 94 | + | |
| 95 | + | |
80 | 96 | | |
81 | 97 | | |
82 | 98 | | |
| |||
117 | 133 | | |
118 | 134 | | |
119 | 135 | | |
120 | | - | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
121 | 142 | | |
122 | | - | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
123 | 146 | | |
124 | | - | |
125 | | - | |
126 | | - | |
| 147 | + | |
127 | 148 | | |
128 | | - | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
129 | 152 | | |
130 | | - | |
131 | | - | |
132 | | - | |
| 153 | + | |
133 | 154 | | |
134 | | - | |
| 155 | + | |
135 | 156 | | |
136 | | - | |
| 157 | + | |
137 | 158 | | |
138 | | - | |
139 | | - | |
140 | | - | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
141 | 165 | | |
142 | 166 | | |
143 | 167 | | |
| |||
0 commit comments