Conversation
The url field from POST /api/download, /api/info, and /api/playlist was passed straight into the yt-dlp argv list. Since these endpoints are unauthenticated, a caller could pass a value like "--exec=<cmd>" instead of a real URL; yt-dlp parses any argv item starting with "-" as an option rather than a positional URL, so this allowed arbitrary command execution on the host after a (fake) download. Fixes: - Add is_safe_url() to require an http(s) scheme + host, rejecting anything that could be interpreted as a CLI flag. - Insert a "--" argv separator before the URL in every yt-dlp invocation so option parsing stops regardless of URL content (defense in depth alongside the scheme check). - Apply the check on all three endpoints that accept a URL.
kesonglab
left a comment
There was a problem hiding this comment.
Reviewed carefully and verified the two defenses empirically (yt-dlp 2026.08.19):
** separator (empirically confirmed):** I tested yt-dlp locally. Without , a value like --exec=echo is parsed as a CLI option (cmd runs it); with -- preceding it, yt-dlp treats it as a URL (ERROR: '--exec=echo' is not a valid URL). So the separator genuinely closes the argument-injection path. Solid.
is_safe_url (verified): urlparse correctly rejects ----prefix strings (empty scheme => false), ftp/file/javascript schemes, and empty-netloc forms; accepts plain http(s). This is defense-in-depth on top of the separator, which matters because it also blocks the get_info/get_playlist_info/start_download` entry points.
Approve. One enhancement to consider (non-blocking): is_safe_url validates scheme+netloc but not the host itself — http://user:pass@127.0.0.1/... or RFC-1918/loopback hosts still pass, which leaves room for SSRF or credential-smuggling in a future endpoint. A socket.getaddrinfo allow-list or a private-IP block would harden it; not needed for the current public-facing download flow.
| # "--" stops yt-dlp from treating a URL that begins with "-" as an | ||
| # option (e.g. "--exec=..."), which would otherwise allow arbitrary | ||
| # command execution. | ||
| cmd += ["--", url] |
There was a problem hiding this comment.
The -- separator here is what closes the injection — I verified on yt-dlp 2026.08.19 that a leading '-' value is treated as a URL (not an option) once '--' precedes it. Consider hardening is_safe_url against RFC-1918/loopback hosts (and credential-in-URL like http://user:pass@host) to block SSRF/credential smuggling in future endpoints.
kesonglab
left a comment
There was a problem hiding this comment.
Reviewed carefully and verified both defenses empirically (yt-dlp 2026.08.19):
-- separator (empirically confirmed): I tested yt-dlp locally. Without --, a value like --exec=echo is parsed as a CLI option; with -- preceding it, yt-dlp treats it as a URL (ERROR: '--exec=echo' is not a valid URL). The separator genuinely closes the argument-injection path.
is_safe_url (verified): urlparse correctly rejects leading-- strings (empty scheme => false), ftp/file/javascript schemes, and empty-netloc forms; accepts plain http(s). This is defense-in-depth on top of the separator, and it also covers the get_info/get_playlist_info/start_download entry points.
Approve. One non-blocking enhancement: is_safe_url validates scheme+netloc but not the host itself — http://user:pass@127.0.0.1/... or RFC-1918/loopback hosts still pass, leaving room for SSRF or credential-smuggling in a future endpoint. A private-IP/loopback block (or getaddrinfo-based allow-list) would harden it, though it's not required for the current public-facing download flow.
| # "--" stops yt-dlp from treating a URL that begins with "-" as an | ||
| # option (e.g. "--exec=..."), which would otherwise allow arbitrary | ||
| # command execution. | ||
| cmd += ["--", url] |
There was a problem hiding this comment.
The -- separator here is what closes the injection - verified on yt-dlp 2026.08.19 that a leading '-' value is treated as a URL (not an option) once it precedes it. Consider hardening is_safe_url against RFC-1918/loopback hosts and credential-in-URL (http://user:pass@host) to block SSRF/credential smuggling in future endpoints.
Summary
urlfrom the unauthenticatedPOST /api/download,/api/info, and/api/playlistendpoints was passed straight into theyt-dlpargv list (app.py). Since it's the last positional argument with nothing separating it from option parsing, a caller can submit something like:{"url": "--exec=touch /tmp/pwned; true"}yt-dlptreats any argv item starting with-/--as a flag rather than a URL, so this reaches--exec(or--exec-before-download,--print, etc.) and results in arbitrary command execution on the host as the process user, with no auth required.Fix
is_safe_url()— requires anhttp/httpsscheme and a host, rejecting anything that could parse as a CLI flag.--argv separator before the URL in everyyt-dlpinvocation so option parsing stops at that point regardless of URL content (defense in depth alongside the scheme check)./api/download,/api/info,/api/playlist).Testing
python3 -m py_compile app.pypasses.is_safe_urlrejects--exec=...,-o=..., empty strings, and non-http(s) schemes, and accepts normalhttps://...URLs.Happy to adjust the validation approach (e.g. stricter allow-list of domains) if you'd prefer a different shape for the fix.