Skip to content

Fix: prevent yt-dlp argument injection via unsanitized URL (RCE) - #58

Open
robomello wants to merge 1 commit into
averygan:mainfrom
robomello:fix/ytdlp-arg-injection
Open

robomello wants to merge 1 commit into
averygan:mainfrom
robomello:fix/ytdlp-arg-injection

Conversation

@robomello

Copy link
Copy Markdown

Summary

url from the unauthenticated POST /api/download, /api/info, and /api/playlist endpoints was passed straight into the yt-dlp argv 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-dlp treats 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

  • Added is_safe_url() — requires an http/https scheme and a host, rejecting anything that could parse as a CLI flag.
  • Inserted a -- argv separator before the URL in every yt-dlp invocation so option parsing stops at that point regardless of URL content (defense in depth alongside the scheme check).
  • Applied the check to all three endpoints that accept a URL (/api/download, /api/info, /api/playlist).

Testing

  • python3 -m py_compile app.py passes.
  • Manually verified is_safe_url rejects --exec=..., -o=..., empty strings, and non-http(s) schemes, and accepts normal https://... URLs.

Happy to adjust the validation approach (e.g. stricter allow-list of domains) if you'd prefer a different shape for the fix.

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 kesonglab left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread app.py
# "--" 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]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 kesonglab left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread app.py
# "--" 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]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants