fix: validate icon URL to prevent server-side request forgery (SSRF) - #1590
fix: validate icon URL to prevent server-side request forgery (SSRF)#1590bunlongheng wants to merge 1 commit into
Conversation
|
Thanks for the report and the patch. A couple of things before this can be looked at properly:
|
The add/edit-item handler fetches a user-supplied icon URL with file_get_contents and no host validation, so an attacker can point it at internal services or cloud metadata (e.g. http://169.254.169.254/) and use Heimdall as a request proxy. Heimdall runs without authentication by default, so this is reachable on default installs. Restrict the URL to http/https, resolve the host and reject private or reserved addresses, and stop following redirects so a public URL cannot bounce to an internal one.
|
Thanks for the thorough review, @KodeStar - really appreciate the time. Addressed all three points: 1. Retargeted to 2. Feature tests in
3. LAN self-hosted use case Let me know if you'd like any changes - happy to iterate. |
0124b77 to
c49d8dd
Compare
|
Thanks for this, the guard is the right idea. I could not merge it as-is because the tests fail in CI: the public-IP test does a real fetch to 8.8.8.8 and times out, and the rejection tests expect 422 where a non-JSON POST gets a 302. There was also a related redirect bypass in the website-lookup endpoint that needed the same treatment. I have folded your icon-URL guard into #1599, which puts both fetches behind one shared helper with mocked-transport tests. You are credited in the commit message. That PR closes this one once merged. |
The website-lookup endpoint validated only the initial URL, then let Guzzle follow redirects unchecked. A public URL that answered with a Location pointing at an internal address was fetched and its body returned. The add/edit item icon fetch used file_get_contents with no address check at all. Both now go through a shared App\Helpers\SafeUrlFetcher that: - allows only http(s) URLs - resolves the host (A and AAAA) and refuses any private or reserved address, including IPv4-mapped IPv6 forms - pins the checked address and the URL's actual port via CURLOPT_RESOLVE - follows redirects itself, up to five hops, running the same guard on every Location before requesting it ALLOW_INTERNAL_REQUESTS keeps its meaning and is now read through config so config caching works. Tests use Guzzle's MockHandler so nothing touches the network. Reported by Kashish Topiwala. Icon URL guard based on PR #1590 by Bunlong Heng.
What
The add/edit-item handler (
ItemController@store/@update) fetches a user-supplied icon URL:There's no check that the URL's host isn't internal, so a request like
icon=http://169.254.169.254/latest/meta-data/x.pngoricon=http://127.0.0.1:PORT/x.pngmakes Heimdall issue that request server-side (SSRF). The image validation only decides whether the response body is stored, so blind SSRF (internal port scan, cloud metadata, hitting internal-only HTTP services) works regardless.verify_peeris also disabled.Since Heimdall runs without authentication by default, this is reachable on a default install.
Fix
http/https.FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE(covers loopback, link-local169.254.0.0/16, and RFC1918).302to an internal one.Legitimate remote icons (public hosts) work exactly as before.
Verification
Ran the exact guard logic under PHP 8.5 against representative payloads - 9/9 as expected:
http://169.254.169.254/...(metadata)http://127.0.0.1/...(loopback)http://10/172.16/192.168...(private)ftp://,file://http://8.8.8.8/..., public IPsphp -lclean.Honest scope / caveats
This closes the practical SSRF vectors (metadata, loopback, RFC1918). Two residual notes so it's not oversold: there's a small DNS-rebinding window because resolution and fetch aren't atomic (redirects are disabled to reduce that), and PHP's
filter_varIPv6 reserved handling has known edges. Happy to iterate on the approach - e.g. centralizing this into a helper or using a stricter resolver - if you'd prefer.