Commit Graph

6 Commits

Author SHA1 Message Date
Alex Shnitman 7082858237 fix: judge the IPv4 tunnelled inside IPv6 transition addresses
The SSRF guard classified addresses with ipaddress.is_global, which looks only
at the outer address. An IPv6 form that carries an IPv4 address at a fixed
offset therefore passed a check the bare address would have failed: the NAT64
well-known prefix 64:ff9b::/96 sits in the 2000::/3 global unicast range, so
64:ff9b::a9fe:a9fe was accepted while 169.254.169.254 was refused. The
deprecated IPv4-compatible form ::/96 has the same property. Both the ingress
validator and the connect-time socket guard classified through the same helper,
so both were affected.

Judge every address a verdict has to account for: the outer address plus any
IPv4 it tunnels, allowed only when all of them are global. Unwrapping this way
can only tighten the verdict, which matters for 6to4 and Teredo — Python
already rejects 2002::/16 and 2001::/32 wholesale, and replacing the outer
address with its payload would have turned 2002:0808:0808:: from blocked into
allowed. Reaching an internal service this way additionally requires NAT64
routing on the host network, which the attacker does not control.

Reported by tonghuaroot in GHSA-5mq5-qr7m-f4wx.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 16:37:57 +02:00
Alex Shnitman 482381d6b9 fix: scope the connect-time loopback allowance to the configured proxy
The connect-time guard permitted every loopback address so that a locally
configured proxy (proxy: http://127.0.0.1:9050) stayed reachable. But the
connect guard is the only check that media URLs derived from remote metadata
ever face — validate_url sees just the submitted URL — so that blanket
allowance let a remote manifest steer the download subprocess at services
bound to the server's loopback interface, with the response written to the
download directory and served back by the UI.

Permit loopback only at the host:port of a proxy the operator configured,
taken from yt-dlp's proxy option and the *_proxy environment variables.
Nothing is lost: when a proxy is in use yt-dlp hands it the media URL instead
of resolving that URL locally, so the two cases never overlap. Every other
loopback destination now falls under the same is_global policy as the rest.

Reported by m3rl1nu5 (https://github.com/hai135) in GHSA-73g4-qhhq-c32c.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 22:44:23 +03:00
Alex Shnitman e061a8a5ba feat: ALLOW_PRIVATE_ADDRESSES to opt out of the SSRF checks (closes #1036)
The SSRF guard rejects any address that isn't globally routable, which breaks
proxy/VPN setups that resolve hosts into private or special-use ranges. The
reported case is Fake-IP clients (sing-box, Clash, Mihomo) that map YouTube to
the RFC 2544 benchmarking range 198.18.0.0/15 to tunnel the traffic; MeTube
rejected it with "Refusing to fetch internal address" even though yt-dlp itself
handles it fine.

Add a boolean ALLOW_PRIVATE_ADDRESSES (default false) that, when set, skips both
layers: validate_url returns after scheme validation without the internal-host
checks, and the connect-time socket guard is not installed. Threaded to the
download subprocess via Download so the guard sees it too. Scheme validation
(http/https only) still applies. Documented in the README as a trust-your-network
opt-out that disables SSRF protection.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 07:44:38 +03:00
Alex Shnitman 1b02a99510 fix: re-validate outbound connections at fetch time against internal hosts
validate_url only inspects the submitted URL string. yt-dlp then follows HTTP
redirects and resolves media URLs from remote metadata without re-checking, so
an allowed URL that 302s to http://169.254.169.254/ (cloud metadata) or an
RFC1918 host is still fetched — the guard's own docstring scoped this out.

Install a getaddrinfo guard in the download subprocess that re-validates every
resolved address at actual connect time, covering redirects and DNS rebinding
for any backend resolving through Python's socket module (urllib, requests).
Loopback is permitted so locally-configured proxies keep working; link-local,
RFC1918 and unique-local are blocked. Native resolvers (curl_cffi/libcurl via
--impersonate) bypass this and rely on network isolation as the backstop.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 06:55:29 +03:00
Alex Shnitman ebcfe577bc fix: fail closed when an SSRF-guarded host cannot be resolved
validate_url() previously returned None (allow) on socket.gaierror, so a
host that failed to resolve at check time was passed straight to yt-dlp. A
host we cannot resolve is a host we cannot verify as non-internal, and it may
resolve differently when yt-dlp fetches it. Reject it instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 23:45:08 +03:00
Alex Shnitman 6d0528783c fix: block SSRF via user-submitted URLs
User-submitted URLs were passed straight to yt-dlp's generic extractor,
letting the server fetch internal endpoints (cloud metadata, loopback,
RFC1918 hosts). Add a url_guard.validate_url check at every URL ingress
(add, subscribe, and nested playlist recursion) that rejects non-http(s)
schemes and hosts resolving to non-global addresses, while leaving bare
video IDs and search prefixes untouched.
2026-07-16 21:31:51 +03:00