VYPR
Medium severity6.1GHSA Advisory· Published Jun 16, 2026· Updated Jun 16, 2026

yt-dlp: File Downloader cookie leak with curl

CVE-2026-50019

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

2

Patches

1
272657252023

[fd/external] `curl`: Fix cookie leak on redirect

https://github.com/yt-dlp/yt-dlpSimon SawickiJun 7, 2026via ghsa
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

News mentions

0

No linked articles in our index yet.