PG1204 opened a new issue, #9103:
URL: https://github.com/apache/texera/issues/9103

   ### What happened?
   
   `GET /api/huggingface/media-proxy?url=…` 
(`HuggingFaceModelResource.proxyRemoteMedia`, 
`amber/src/main/scala/org/apache/texera/web/resource/HuggingFaceModelResource.scala`
 L104) fetches a remote media URL on the server and returns the body to the 
caller. To prevent SSRF it only accepts URLs whose host is in 
`ALLOWED_MEDIA_HOST_SUFFIXES` (huggingface.co, fal.media, replicate.delivery, 
replicate.com, and their subdomains).
   
   That check only runs on the URL as submitted. The fetch itself is 
`Unirest.get(trimmedUrl)` (L128-132), and Unirest 3.x follows redirects by 
default (`Config.followRedirects` is `true`). Nothing in amber turns this off. 
So if an allowlisted host answers with a redirect, the proxy follows it 
anywhere, including `http://169.254.169.254/...` or an internal service, and 
returns that response body to the caller. This is the same bypass that #6967 
reported for the operator's Python-side fetch (fixed in #7013). The backend 
proxy was not covered.
   
   The same check has two smaller gaps:
   - The allowlist is purely lexical. The resolved IP is never checked, so an 
allowlisted name that resolves to a private or loopback address is fetched.
   - `http://` is accepted, so anyone on the network path can redirect a 
plaintext request to an allowlisted host. Separately, the scheme check is 
case-sensitive, so `HTTPS://…` is rejected. That is harmless, but inconsistent 
with the case-insensitive host check.
   
   Any logged-in user can call the endpoint directly with any URL. The result 
panel also calls it for media URLs found in result cells.
   
   Exploiting this needs a redirect from an allowlisted domain: an open 
redirect on one of them, a takeover of a dangling subdomain (the suffix match 
admits every subdomain), or an on-path attacker for `http://`. I have not found 
an open redirect on the four domains today. The problem is that the allowlist 
currently protects nothing once one exists.
   
   ### Proposed fix
   
   - Fetch with redirects disabled and follow at most a few hops (e.g. 5) 
manually, re-running the scheme and host checks on every hop. Unirest 3.x has 
no per-request redirect setting, so this needs a dedicated `UnirestInstance` 
(`Unirest.spawnInstance()`) rather than changing the global config the other HF 
endpoints use. `S3ProxyServlet` in file-service already disables redirects 
(`followRedirects(HttpClient.Redirect.NEVER)`).
   - Add `hf.co` to the allowlist. Standard Hub file URLs 
(`https://huggingface.co/<repo>/resolve/main/<file>`) currently redirect to 
`us.aws.cdn.hf.co`, so re-checking each hop without it would break the most 
common Hugging Face media URL.
   - Accept `https://` only, compared case-insensitively. 
`http://huggingface.co` already redirects to https, so no Hugging Face URL is 
lost.
   - Reject hosts that resolve to private, loopback, link-local or other 
non-public addresses, as #7013 does on the Python side.
   - Pin the behavior in `HuggingFaceModelResourceSpec`: a redirect to a 
non-allowlisted host is refused, a redirect within the allowlist is followed, 
too many hops are refused, `http://` is rejected, and `HTTPS://` and `hf.co` 
are accepted.
   
   Related: the proxy returns the upstream status code to the caller 
(L154-156), which lets it be used to probe what is reachable. That can be 
handled in the same change or separately.
   
   
   ### How to reproduce?
   
   Reproduced locally against Unirest 3.14.5 (the version in 
`amber/build.sbt`), with the endpoint's checks copied verbatim:
   
   1. Map an allowlisted name to localhost for the server process, e.g. 
`127.0.0.1 huggingface.co` in `/etc/hosts` (or `-Djdk.net.hosts.file` on JDK 
18+). This stands in for an allowlisted host that returns a redirect the 
attacker controls.
   2. Start a server on `127.0.0.1:A` that answers every request with `302 
Location: http://127.0.0.1:B/latest/meta-data`, and an "internal" server on 
`127.0.0.1:B` that returns a secret string.
   3. Request 
`/api/huggingface/media-proxy?url=http://huggingface.co:A/img.png`.
   
   | Request | Result |
   |---|---|
   | `http://127.0.0.1:B/...` directly | 403, blocked as expected |
   | allowlisted host → 302 → `127.0.0.1:B` | 200, internal body returned to 
the caller |
   | `http://cdn-lfs.huggingface.co:B/...` resolving to 127.0.0.1, no redirect 
| 200, internal body returned |
   | the redirect case with `followRedirects(false)` | the upstream 302 is 
passed back as the response status |
   
   ### Version/Branch
   
   1.4.0-incubating-SNAPSHOT (main)
   
   ### Commit Hash (Optional)
   
   _No response_
   
   ### What browsers are you seeing the problem on?
   
   _No response_
   
   ### Relevant log output
   
   ```shell
   
   ```


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to