yt-dlp: File Downloader cookie leak with curl
Description
yt-dlp using curl as external downloader leaks cookies to unintended hosts on HTTP redirect or mismatched fragment hosts, fixed in version 2026.06.09.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
yt-dlp using curl as external downloader leaks cookies to unintended hosts on HTTP redirect or mismatched fragment hosts, fixed in version 2026.06.09.
Vulnerability
In yt-dlp, when curl is used as an external downloader via --downloader curl, cookies are passed to curl using the --cookie option. However, because the cookies are not loaded from a file, curl does not activate its cookie engine and sends cookies with requests to any domain or path, ignoring cookie scope. This vulnerability affects yt-dlp versions since 2023.09.24 [1][2][4].
Exploitation
An attacker can craft a malicious website containing a URL that yt-dlp detects as a video download. This URL points to a trusted site for which the user has stored cookies and performs an unvalidated HTTP redirect to an attacker-controlled server. yt-dlp extracts the URL, calculates the relevant cookies, and passes them to curl. When curl follows the redirect, it forwards the user's cookies to the attacker's server [4].
Impact
A successful attack allows the attacker to obtain the user's cookies for the trusted site. This can lead to session hijacking, unauthorized access to the user's accounts, or other actions that rely on the stolen credentials [4].
Mitigation
The issue is fixed in yt-dlp version 2026.06.09. The fix passes cookies to curl via stdin using --cookie - when curl is version 7.59 or higher, via --cookie /dev/fd/0 if the system supports that device file, or otherwise via a temporary file. Users who cannot upgrade should avoid using --downloader curl [1][2][4].
AI Insight generated on Jun 16, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected products
2Patches
1272657252023[fd/external] `curl`: Fix cookie leak on redirect
3 files changed · +156 −28
test/test_downloader_external.py+104 −18 modified@@ -8,8 +8,15 @@ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) import http.cookiejar +import http.server +import ipaddress +import pytest +import json +import tempfile +import threading from test.helper import FakeYDL +from yt_dlp.networking.common import HTTPHeaderDict from yt_dlp.downloader.external import ( Aria2cFD, AxelFD, @@ -75,34 +82,113 @@ class TestWgetFD(unittest.TestCase): def test_make_cmd(self): with FakeYDL() as ydl: downloader = WgetFD(ydl, {}) - self.assertNotIn('--load-cookies', downloader._make_cmd('test', TEST_INFO)) - # Test cookiejar tempfile arg is added ydl.cookiejar.set_cookie(http.cookiejar.Cookie(**TEST_COOKIE)) - self.assertIn('--load-cookies', downloader._make_cmd('test', TEST_INFO)) - - -class TestCurlFD(unittest.TestCase): - def test_make_cmd(self): + assert '--load-cookies' in downloader._make_cmd('test', TEST_INFO) + + +class HTTPTestHandler(http.server.BaseHTTPRequestHandler): + def do_GET(self, /): + if self.path.startswith('/redirect'): + target = self.headers.get('X-Redirect-Location') + if not target: + self.send_error(500) + return + self.send_response(301) + self.send_header('Location', target) + self.end_headers() + + elif self.path == '/headers': + self.send_response(200) + self.end_headers() + self.wfile.write(json.dumps(list(self.headers.items())).encode()) + +class HTTPTestServer(http.server.HTTPServer): + @property + def address(self, /): + return ipaddress.ip_address(self.server_address[0]) + + @property + def uri(self, /): + addr, port, *_ = self.server_address + if ':' in addr: + addr = f'[{addr}]' + return f'http://{addr}:{port}' + + def __enter__(self, /): + result = super().__enter__() + thread = threading.Thread(target=self.serve_forever) + thread.start() + return result + + def __exit__(self, /, *exc): + self.shutdown() + return super().__exit__(*exc) + + +class TestDownloaderCookieBehavior: + @pytest.mark.parametrize('downloader_cls', [ + pytest.param(CurlFD, marks=pytest.mark.skipif(not CurlFD.available() or CurlFD._curl_version < CurlFD._MIN_VERSION_FOR_STDIN_COOKIES, reason='curl unavailable or too old')), + pytest.param(WgetFD, marks=pytest.mark.skipif(not WgetFD.available(), reason='wget unavailable')), + pytest.param(Aria2cFD, marks=pytest.mark.skipif(not Aria2cFD.available(), reason='aria2c unavailable')), + ]) + def test_cookie_behavior(self, /, downloader_cls): with FakeYDL() as ydl: - downloader = CurlFD(ydl, {}) - self.assertNotIn('--cookie', downloader._make_cmd('test', TEST_INFO)) - # Test cookie header is added - ydl.cookiejar.set_cookie(http.cookiejar.Cookie(**TEST_COOKIE)) - self.assertIn('--cookie', downloader._make_cmd('test', TEST_INFO)) - self.assertIn('test=ytdlp', downloader._make_cmd('test', TEST_INFO)) + downloader = downloader_cls(ydl, {}) + + with HTTPTestServer(('localhost', 0), HTTPTestHandler) as server_a: + second_addr = server_a.address + 1 + if not second_addr.is_loopback: + second_addr = server_a.address - 1 + assert second_addr.is_loopback, f'failed to find derived loopback address for {server_a.address}' + + ydl.cookiejar.set_cookie(http.cookiejar.Cookie( + 1, + 'c', + 'test', + server_a.server_address[1], + True, + str(server_a.address), + True, + False, + '/', + False, + False, + 0, + True, + None, + None, + {}, + )) + + with tempfile.NamedTemporaryFile(delete=False) as file: + file.close() + assert downloader.real_download(file.name, {'url': f'{server_a.uri}/headers'}), 'Expected download (/headers) to succeed' + + with open(file.name, 'rb') as f: + data = HTTPHeaderDict(json.load(f)) + assert 'c=test' in data.get('Cookie', '').split(';'), 'Expected cookie to be set in initial request' + + with HTTPTestServer((str(second_addr), 0), HTTPTestHandler) as server_b: + assert downloader.real_download(file.name, { + 'url': f'{server_a.uri}/redirect', + 'http_headers': { + 'X-Redirect-Location': f'{server_b.uri}/headers', + }, + }), 'Expected download (/redirect) to succeed' + + with open(file.name, 'rb') as f: + data = HTTPHeaderDict(json.load(f)) + + assert data.get('Cookie') is None, 'Expected cookie to be unset in redirected request' class TestAria2cFD(unittest.TestCase): def test_make_cmd(self): with FakeYDL() as ydl: downloader = Aria2cFD(ydl, {}) - downloader._make_cmd('test', TEST_INFO) - self.assertFalse(hasattr(downloader, '_cookies_tempfile')) - - # Test cookiejar tempfile arg is added ydl.cookiejar.set_cookie(http.cookiejar.Cookie(**TEST_COOKIE)) cmd = downloader._make_cmd('test', TEST_INFO) - self.assertIn(f'--load-cookies={downloader._cookies_tempfile}', cmd) + assert f'--load-cookies={downloader._cookies_tempfile}' in cmd @unittest.skipUnless(FFmpegFD.available(), 'ffmpeg not found')
yt_dlp/downloader/external.py+48 −8 modified@@ -1,5 +1,6 @@ import enum import functools +import io import json import os import re @@ -16,6 +17,7 @@ Popen, RetryManager, _configuration_args, + _get_exe_version_output, check_executable, classproperty, cli_bool_option, @@ -26,6 +28,7 @@ find_available_port, remove_end, traverse_obj, + version_tuple, ) @@ -136,7 +139,9 @@ def _write_cookies(self): self._cookies_tempfile = tmp_cookies.name self.to_screen(f'[download] Writing temporary cookies file to "{self._cookies_tempfile}"') # real_download resets _cookies_tempfile; if it's None then save() will write to cookiejar.filename - self.ydl.cookiejar.save(self._cookies_tempfile) + self.ydl.cookiejar.save(self._cookies_tempfile, True, True) + with open(self.ydl.cookiejar.filename or self._cookies_tempfile, "r") as file: + print("cookies", repr(file.read())) return self.ydl.cookiejar.filename or self._cookies_tempfile def _call_downloader(self, tmpfilename, info_dict): @@ -195,12 +200,39 @@ def _call_process(self, cmd, info_dict): class CurlFD(ExternalFD): AVAILABLE_OPT = '-V' _CAPTURE_STDERR = False # curl writes the progress to stderr + _MIN_VERSION_FOR_STDIN_COOKIES = (7, 59) + + @classmethod + def available(cls, path=None): + if path is None: + path = 'curl' + output = _get_exe_version_output(path, ['-V']) + if not output: + return False + parts = output.split(' ', maxsplit=2) + if len(parts) < 3: + return False + + cls.exe = path + cls._curl_version = version_tuple(parts[1]) + return path def _make_cmd(self, tmpfilename, info_dict): cmd = [self.exe, '--location', '-o', tmpfilename, '--compressed'] - cookie_header = self.ydl.cookiejar.get_cookie_header(info_dict['url']) - if cookie_header: - cmd += ['--cookie', cookie_header] + + if self._curl_version >= self._MIN_VERSION_FOR_STDIN_COOKIES: + # Supports `--cookies -` + cmd += ['--cookie', '-'] + elif os.path.islink('/dev/fd/0'): + cmd += ['--cookie', '/dev/fd/0'] + else: + cookies_file = self._write_cookies() + if '=' in cookies_file: + # XXX: what to raise here? + raise RuntimeError('curl version too old or temp directory contains `=`; please use another downloader or update curl') + assert cookies_file != '-' + cmd += ['--cookie', cookies_file] + if info_dict.get('http_headers') is not None: for key, val in info_dict['http_headers'].items(): cmd += ['--header', f'{key}: {val}'] @@ -222,6 +254,16 @@ def _make_cmd(self, tmpfilename, info_dict): cmd += ['--', info_dict['url']] return cmd + def _call_process(self, cmd, info_dict): + if self._curl_version > self._MIN_VERSION_FOR_STDIN_COOKIES or os.path.islink('/dev/fd/0'): + # Supports `--cookies -` or reading from device file as `--cookies /dev/fd/0` + buffer = io.StringIO() + self.ydl.cookiejar._really_save(buffer, True, True) + return Popen.run(cmd, text=True, input=buffer.getvalue()) + + # Cookies already passed via cookiesfile + return Popen.run(cmd, text=True) + class AxelFD(ExternalFD): AVAILABLE_OPT = '-V' @@ -244,8 +286,7 @@ class WgetFD(ExternalFD): def _make_cmd(self, tmpfilename, info_dict): cmd = [self.exe, '-O', tmpfilename, '-nv', '--compression=auto'] - if self.ydl.cookiejar.get_cookie_header(info_dict['url']): - cmd += ['--load-cookies', self._write_cookies()] + cmd += ['--load-cookies', self._write_cookies()] if info_dict.get('http_headers') is not None: for key, val in info_dict['http_headers'].items(): cmd += ['--header', f'{key}: {val}'] @@ -301,8 +342,7 @@ def _make_cmd(self, tmpfilename, info_dict): else: cmd += ['--min-split-size', '1M'] - if self.ydl.cookiejar.get_cookie_header(info_dict['url']): - cmd += [f'--load-cookies={self._write_cookies()}'] + cmd += [f'--load-cookies={self._write_cookies()}'] if info_dict.get('http_headers') is not None: for key, val in info_dict['http_headers'].items(): cmd += ['--header', f'{key}: {val}']
yt_dlp/utils/_utils.py+4 −2 modified@@ -915,10 +915,12 @@ def kill(self, *, timeout=0): self.wait(timeout=timeout) @classmethod - def run(cls, *args, timeout=None, **kwargs): + def run(cls, *args, timeout=None, input=None, **kwargs): + if input is not None and kwargs.get('stdin') is None: + kwargs['stdin'] = subprocess.PIPE with cls(*args, **kwargs) as proc: default = '' if proc.__text_mode else b'' - stdout, stderr = proc.communicate_or_kill(timeout=timeout) + stdout, stderr = proc.communicate_or_kill(input=input, timeout=timeout) return stdout or default, stderr or default, proc.returncode
Vulnerability mechanics
Root cause
"Passing cookies via `--cookie <header_string>` without activating curl's cookie engine causes curl to send cookies to any domain or path on redirect."
Attack vector
An attacker embeds a URL on a malicious website that yt-dlp detects as a video download. That URL points to a trusted domain for which the user has stored cookies, and the server performs an unvalidated HTTP redirect to an attacker-controlled host [ref_id=1]. yt-dlp extracts the URL, computes the cookies, and passes them to curl via `--cookie`. Because the cookie engine is not activated, curl sends the cookies to the redirect target, leaking sensitive cookie data [CWE-200].
Affected code
The vulnerability is in `yt_dlp/downloader/external.py` in the `CurlFD._make_cmd` method. The old code passed cookies via `--cookie <header_string>`, which does not activate curl's cookie engine, so curl sends the cookie to any domain or path regardless of scope. The patch also modifies `_call_process` to pipe cookies via stdin when curl ≥ 7.59 or `/dev/fd/0` is available.
What the fix does
The patch changes how cookies are passed to curl. For curl ≥ 7.59, cookies are piped via stdin using `--cookie -`; otherwise, if `/dev/fd/0` is a symlink, `--cookie /dev/fd/0` is used; as a last resort, a temporary file is written and passed via `--cookie <file>` [patch_id=6193542]. This ensures curl's cookie engine is activated, so cookies are only sent to the domain and path for which they are scoped, preventing leakage on redirect. The `_call_process` method is also overridden to pipe the cookie data from the cookiejar.
Preconditions
- configyt-dlp must be configured to use curl as the external downloader (e.g., `--downloader curl`)
- authThe user must have cookies stored for a trusted domain that an attacker can reference
- networkThe attacker must control a server that the initial URL redirects to
- inputThe initial URL must be one that yt-dlp extracts as a video download
Generated on Jun 16, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-f7j3-774f-rfhjghsaADVISORY
- github.com/yt-dlp/yt-dlp-nightly-builds/releases/tag/2026.06.09.230517ghsa
- github.com/yt-dlp/yt-dlp/commit/2726572520238356bcf64aba2040228648b44c82ghsa
- github.com/yt-dlp/yt-dlp/releases/tag/2026.06.09ghsa
- github.com/yt-dlp/yt-dlp/security/advisories/GHSA-f7j3-774f-rfhjghsa
News mentions
0No linked articles in our index yet.