v0.75.2: endpoint URLs can no longer name a private address
This is a security patch with one change in it, written entirely by the maintainer and reported by an outside contributor. Like v0.75.1, it is a backport rather than a cut of the development branch: its tree is the v0.75.1 tag plus eight commits, and everything merged since v0.75.0 is left for the next minor release. Upstream gave it no codename.
It fixes advisory GHSA-9f49-hqpf-c849, rated medium (CVSS 4.1, server-side request forgery), affecting 0.3.0 through 0.75.1. It was reported by @abdugafforov-bobur, with a clear reproduction.
Re-run this
Nothing. Unlike the release before it, no finished run, adapter, checkpoint or verdict is affected, and no credential needs rotating because of this advisory: the half that sent OPENAI_API_KEY to a judge host was v0.75.1's. What can go wrong on upgrade is a command that now refuses, loudly, as described below.
What was wrong
Several outbound endpoint checks refused plain http:// to a non-loopback host but accepted any https:// URL, including one whose host was a private, link-local, unique-local, shared (RFC 6598) or otherwise non-public IP address, in any spelling: abbreviated, decimal, hex or octal IPv4, IPv4-mapped IPv6, or digits and label separators that an HTTP client folds to ASCII before it connects. A configuration file that set a judge URL could therefore make the machine that ran it send a chat-completions POST to an internal HTTPS service addressed by IP. TLS verification was unchanged, so the service had to present a certificate for that address that the machine trusted, and its response was not shown to whoever wrote the file.
What refuses now
All of these now refuse a private, link-local or reserved IP address on https as well as http, through one shared helper:
| Where the URL comes from | Setting |
|---|---|
soup data generate | --api-base (providers openai and server) |
| The vLLM provider and the commands that build on it | --provider vllm |
soup eval judge and every judge the CLI builds | --api-base, soup data best-of-n --judge |
| Eval-gate suites | judge_model |
soup ship | --judge-model, eval.ship.judge_model |
| Online DPO | training.online_dpo_judge, now also checked when soup.yaml loads, not only when the trainer starts |
| The Web UI chat proxy | the endpoint you type |
Loopback still works. http://0.0.0.0 is no longer treated as local by soup data generate --api-base and the chat proxy; use localhost or 127.0.0.1. The shared predicate also now counts 100.64.0.0/10 (shared address space, which includes Tailscale) and fec0::/10 as non-public, for webhooks, the OTLP endpoint, telemetry, hub endpoints over plain HTTP and soup ingest --pull, where --allow-private-host admits them. An address written with non-ASCII digits or label separators is classified as the address it folds to.
If your config names a private IP address
It fails, with an error naming the setting. Loopback and public addresses are not affected. Examples, verbatim:
soup data generate:Generation error: api_base: private/link-local/reserved IP hosts are not allowed (SSRF protection)(exit 1)soup eval judge:judge URL: private/link-local/reserved IP hosts are not allowed (SSRF protection)(exit 1)soup ship:Error: --judge-model: private/link-local/reserved IP hosts are not allowed (SSRF protection)(exit 3, and now before either model loads)training.online_dpo_judge:training -> online_dpo_judge: Value error, online_dpo_judge: private/link-local/reserved IP hosts are not allowed (SSRF protection), printed under aConfig validation error:heading (exit 1)- Web UI chat: HTTP 400,
endpoint: private/link-local/reserved IP hosts are not allowed (SSRF protection)
Address the server by its hostname instead. That works over HTTPS with a certificate valid for that name. Plain http is accepted only for loopback in these settings, as before, except that two of them used to count http://0.0.0.0 as local and no longer do. There is no opt-in for an IP address in a private range; the one flag of this kind, --allow-private-host, belongs to soup ingest --pull. The refusal names no remedy, which upstream tracks as #1549.
What did not change, and what is still open
- Hostnames are not resolved. A hostname that resolves to a private address is still accepted. This narrows what a URL can name directly; it does not make internal services unreachable by name.
HF_ENDPOINTand the hub endpoint variables refuse any non-loopback host over plain HTTP, and do not check the address over HTTPS. They are read from your own environment, so anhttpsendpoint naming a private address is accepted there, and the v0.75.2 refusal does not change that.- The checks are found by their shape, so a new outbound call could skip them. Upstream tracks a ratchet over every outbound call site as #1547.
- Loopback spellings differ between settings.
http://[::1]passes the judge and vLLM checks, and not eval-gate suites,soup shipor the online-DPO trainer (#1548). - IP-address parsing depends on the platform's
inet_atonand, for 6to4, NAT64 and Teredo forms, on the Python interpreter's address tables (#1550).
Until you upgrade, upstream's advice is not to run an untrusted soup.yaml, eval-gate suite or .can package that sets judge_model, online_dpo_judge or eval.ship.judge_model, and to keep soup ui bound to loopback with its token private.
Also fixed
soup shipchecks--judge-modelbefore it builds the base and tuned models. It used to load both and only then stop with a usage error.- The Web UI chat proxy answers 400, not 500, for an endpoint URL that does not parse.
What was measured, and what was not
Nothing was measured, and there is no gate record. Every item is a hardening or correctness change with its own tests. The preprint is unchanged. Test files go from 500 to 501.
See also
- v0.75.1: an adapter with nothing in it, and eight hardening fixes: the release before this one, and the other advisory.
- Eval-Gated Training: the judge URL allowlist.
- soup ship:
--judge-model. - Web UI: the chat endpoint.
- What's new across v0.71 to v0.75.
Soup is free and Apache-2.0. If it saved you a training run, starring the repo costs nothing and helps most. You can also fund the GPU time behind the work a 4 GB laptop cannot reach.