VYPR
Moderate severityNVD Advisory· Published Jul 6, 2023· Updated Feb 13, 2025

yt-dlp File Downloader cookie leak

CVE-2023-35934

Description

yt-dlp prior to July 6, 2023 leaks cookies on HTTP redirects and fragmented downloads due to improper scoping, allowing potential cookie theft.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

yt-dlp prior to July 6, 2023 leaks cookies on HTTP redirects and fragmented downloads due to improper scoping, allowing potential cookie theft.

## What the vulnerability is yt-dlp, a command-line video downloader, passes all cookies as a Cookie header to downloaders without proper scoping [1][2]. This means that on HTTP redirects to different hosts, or when downloading fragmented formats like HLS where fragment hosts differ from the manifest host, cookies intended for the original domain may be sent to other domains. The bug is present in versions prior to 2023.07.06 and nightly 2023.07.06.185519 [2].

Exploitation

An attacker can set up a malicious server that responds with redirects to domains where they can capture cookies, or they can manipulate fragment URLs in manifests. No special network position is required beyond being able to serve content that yt-dlp downloads. The user must be tricked into downloading from a malicious source, or an attacker could inject redirects via a compromised site. The vulnerability affects all native and external downloaders except curl and httpie (version 3.1.0+) which handle cookies correctly [2].

Impact

By leaking cookies, an attacker could gain access to authenticated sessions, potentially allowing account takeover or access to private resources. Since cookies may contain sensitive information such as authentication tokens, this could have severe consequences for users who rely on yt-dlp to download content from sites requiring login.

Mitigation

The fix, implemented in commit [3] and [4], includes removing the Cookie header on redirects, using the cookiejar to calculate proper headers, and leveraging external downloaders' built-in cookie support. Users are urged to update to yt-dlp 2023.07.06 or later. For those who cannot upgrade, workarounds include avoiding cookie-based authentication, not using --load-info-json, or using curl as the external downloader [2].

AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
yt-dlpPyPI
< 2023.7.062023.7.06

Affected products

5

Patches

3
312151222848

[core] Change how `Cookie` headers are handled

https://github.com/yt-dlp/yt-dlpSimon SawickiJul 6, 2023via ghsa
3 files changed · +139 4
  • test/test_YoutubeDL.py+56 0 modified
    @@ -1213,6 +1213,62 @@ def _real_extract(self, url):
             self.assertEqual(downloaded['extractor'], 'Video')
             self.assertEqual(downloaded['extractor_key'], 'Video')
     
    +    def test_header_cookies(self):
    +        from http.cookiejar import Cookie
    +
    +        ydl = FakeYDL()
    +        ydl.report_warning = lambda *_, **__: None
    +
    +        def cookie(name, value, version=None, domain='', path='', secure=False, expires=None):
    +            return Cookie(
    +                version or 0, name, value, None, False,
    +                domain, bool(domain), bool(domain), path, bool(path),
    +                secure, expires, False, None, None, rest={})
    +
    +        _test_url = 'https://yt.dlp/test'
    +
    +        def test(encoded_cookies, cookies, headers=False, round_trip=None, error=None):
    +            def _test():
    +                ydl.cookiejar.clear()
    +                ydl._load_cookies(encoded_cookies, from_headers=headers)
    +                if headers:
    +                    ydl._apply_header_cookies(_test_url)
    +                data = {'url': _test_url}
    +                ydl._calc_headers(data)
    +                self.assertCountEqual(
    +                    map(vars, ydl.cookiejar), map(vars, cookies),
    +                    'Extracted cookiejar.Cookie is not the same')
    +                if not headers:
    +                    self.assertEqual(
    +                        data.get('cookies'), round_trip or encoded_cookies,
    +                        'Cookie is not the same as round trip')
    +                ydl.__dict__['_YoutubeDL__header_cookies'] = []
    +
    +            with self.subTest(msg=encoded_cookies):
    +                if not error:
    +                    _test()
    +                    return
    +                with self.assertRaisesRegex(Exception, error):
    +                    _test()
    +
    +        test('test=value; Domain=.yt.dlp', [cookie('test', 'value', domain='.yt.dlp')])
    +        test('test=value', [cookie('test', 'value')], error='Unscoped cookies are not allowed')
    +        test('cookie1=value1; Domain=.yt.dlp; Path=/test; cookie2=value2; Domain=.yt.dlp; Path=/', [
    +            cookie('cookie1', 'value1', domain='.yt.dlp', path='/test'),
    +            cookie('cookie2', 'value2', domain='.yt.dlp', path='/')])
    +        test('test=value; Domain=.yt.dlp; Path=/test; Secure; Expires=9999999999', [
    +            cookie('test', 'value', domain='.yt.dlp', path='/test', secure=True, expires=9999999999)])
    +        test('test="value; "; path=/test; domain=.yt.dlp', [
    +            cookie('test', 'value; ', domain='.yt.dlp', path='/test')],
    +            round_trip='test="value\\073 "; Domain=.yt.dlp; Path=/test')
    +        test('name=; Domain=.yt.dlp', [cookie('name', '', domain='.yt.dlp')],
    +             round_trip='name=""; Domain=.yt.dlp')
    +
    +        test('test=value', [cookie('test', 'value', domain='.yt.dlp')], headers=True)
    +        test('cookie1=value; Domain=.yt.dlp; cookie2=value', [], headers=True, error='Invalid syntax')
    +        ydl.deprecated_feature = ydl.report_error
    +        test('test=value', [], headers=True, error='Passing cookies as a header is a potential security risk')
    +
     
     if __name__ == '__main__':
         unittest.main()
    
  • yt_dlp/downloader/common.py+6 1 modified
    @@ -32,6 +32,7 @@
         timetuple_from_msec,
         try_call,
     )
    +from ..utils.traversal import traverse_obj
     
     
     class FileDownloader:
    @@ -419,7 +420,6 @@ def download(self, filename, info_dict, subtitle=False):
             """Download to a filename using the info from info_dict
             Return True on success and False otherwise
             """
    -
             nooverwrites_and_exists = (
                 not self.params.get('overwrites', True)
                 and os.path.exists(encodeFilename(filename))
    @@ -453,6 +453,11 @@ def download(self, filename, info_dict, subtitle=False):
                 self.to_screen(f'[download] Sleeping {sleep_interval:.2f} seconds ...')
                 time.sleep(sleep_interval)
     
    +        # Filter the `Cookie` header from the info_dict to prevent leaks.
    +        # See: https://github.com/yt-dlp/yt-dlp/security/advisories/GHSA-v8mc-9377-rwjj
    +        info_dict['http_headers'] = dict(traverse_obj(info_dict, (
    +            'http_headers', {dict.items}, lambda _, pair: pair[0].lower() != 'cookie'))) or None
    +
             ret = self.real_download(filename, info_dict)
             self._finish_multiline_status()
             return ret, True
    
  • yt_dlp/YoutubeDL.py+77 3 modified
    @@ -1,9 +1,11 @@
     import collections
     import contextlib
    +import copy
     import datetime
     import errno
     import fileinput
     import functools
    +import http.cookiejar
     import io
     import itertools
     import json
    @@ -25,7 +27,7 @@
     from .cache import Cache
     from .compat import urllib  # isort: split
     from .compat import compat_os_name, compat_shlex_quote
    -from .cookies import load_cookies
    +from .cookies import LenientSimpleCookie, load_cookies
     from .downloader import FFmpegFD, get_suitable_downloader, shorten_protocol_name
     from .downloader.rtmp import rtmpdump_version
     from .extractor import gen_extractor_classes, get_info_extractor
    @@ -673,6 +675,9 @@ def process_color_policy(stream):
             if auto_init and auto_init != 'no_verbose_header':
                 self.print_debug_header()
     
    +        self.__header_cookies = []
    +        self._load_cookies(traverse_obj(self.params.get('http_headers'), 'cookie', casesense=False))  # compat
    +
             def check_deprecated(param, option, suggestion):
                 if self.params.get(param) is not None:
                     self.report_warning(f'{option} is deprecated. Use {suggestion} instead')
    @@ -1625,8 +1630,60 @@ def progress(msg):
                     self.to_screen('')
                 raise
     
    +    def _load_cookies(self, data, *, from_headers=True):
    +        """Loads cookies from a `Cookie` header
    +
    +        This tries to work around the security vulnerability of passing cookies to every domain.
    +        See: https://github.com/yt-dlp/yt-dlp/security/advisories/GHSA-v8mc-9377-rwjj
    +        The unscoped cookies are saved for later to be stored in the jar with a limited scope.
    +
    +        @param data         The Cookie header as string to load the cookies from
    +        @param from_headers If `False`, allows Set-Cookie syntax in the cookie string (at least a domain will be required)
    +        """
    +        for cookie in LenientSimpleCookie(data).values():
    +            if from_headers and any(cookie.values()):
    +                raise ValueError('Invalid syntax in Cookie Header')
    +
    +            domain = cookie.get('domain') or ''
    +            expiry = cookie.get('expires')
    +            if expiry == '':  # 0 is valid
    +                expiry = None
    +            prepared_cookie = http.cookiejar.Cookie(
    +                cookie.get('version') or 0, cookie.key, cookie.value, None, False,
    +                domain, True, True, cookie.get('path') or '', bool(cookie.get('path')),
    +                cookie.get('secure') or False, expiry, False, None, None, {})
    +
    +            if domain:
    +                self.cookiejar.set_cookie(prepared_cookie)
    +            elif from_headers:
    +                self.deprecated_feature(
    +                    'Passing cookies as a header is a potential security risk; '
    +                    'they will be scoped to the domain of the downloaded urls. '
    +                    'Please consider loading cookies from a file or browser instead.')
    +                self.__header_cookies.append(prepared_cookie)
    +            else:
    +                self.report_error('Unscoped cookies are not allowed; please specify some sort of scoping',
    +                                  tb=False, is_error=False)
    +
    +    def _apply_header_cookies(self, url):
    +        """Applies stray header cookies to the provided url
    +
    +        This loads header cookies and scopes them to the domain provided in `url`.
    +        While this is not ideal, it helps reduce the risk of them being sent
    +        to an unintended destination while mostly maintaining compatibility.
    +        """
    +        parsed = urllib.parse.urlparse(url)
    +        if not parsed.hostname:
    +            return
    +
    +        for cookie in map(copy.copy, self.__header_cookies):
    +            cookie.domain = f'.{parsed.hostname}'
    +            self.cookiejar.set_cookie(cookie)
    +
         @_handle_extraction_exceptions
         def __extract_info(self, url, ie, download, extra_info, process):
    +        self._apply_header_cookies(url)
    +
             try:
                 ie_result = ie.extract(url)
             except UserNotLive as e:
    @@ -2414,9 +2471,24 @@ def _calc_headers(self, info_dict):
             if 'Youtubedl-No-Compression' in res:  # deprecated
                 res.pop('Youtubedl-No-Compression', None)
                 res['Accept-Encoding'] = 'identity'
    -        cookies = self.cookiejar.get_cookie_header(info_dict['url'])
    +        cookies = self.cookiejar.get_cookies_for_url(info_dict['url'])
             if cookies:
    -            res['Cookie'] = cookies
    +            encoder = LenientSimpleCookie()
    +            values = []
    +            for cookie in cookies:
    +                _, value = encoder.value_encode(cookie.value)
    +                values.append(f'{cookie.name}={value}')
    +                if cookie.domain:
    +                    values.append(f'Domain={cookie.domain}')
    +                if cookie.path:
    +                    values.append(f'Path={cookie.path}')
    +                if cookie.secure:
    +                    values.append('Secure')
    +                if cookie.expires:
    +                    values.append(f'Expires={cookie.expires}')
    +                if cookie.version:
    +                    values.append(f'Version={cookie.version}')
    +            info_dict['cookies'] = '; '.join(values)
     
             if 'X-Forwarded-For' not in res:
                 x_forwarded_for_ip = info_dict.get('__x_forwarded_for_ip')
    @@ -3423,6 +3495,8 @@ def download_with_info_file(self, info_filename):
                 infos = [self.sanitize_info(info, self.params.get('clean_infojson', True))
                          for info in variadic(json.loads('\n'.join(f)))]
             for info in infos:
    +            self._load_cookies(info.get('cookies'), from_headers=False)
    +            self._load_cookies(traverse_obj(info.get('http_headers'), 'Cookie', casesense=False))  # compat
                 try:
                     self.__download_wrapper(self.process_ie_result)(info, download=True)
                 except (DownloadError, EntryNotInPlaylist, ReExtractInfo) as e:
    
1ceb657bdd25

[fd/external] Scope cookies

https://github.com/yt-dlp/yt-dlpbashonlyJul 5, 2023via ghsa
3 files changed · +179 2
  • test/test_downloader_external.py+133 0 added
    @@ -0,0 +1,133 @@
    +#!/usr/bin/env python3
    +
    +# Allow direct execution
    +import os
    +import sys
    +import unittest
    +
    +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
    +
    +import http.cookiejar
    +
    +from test.helper import FakeYDL
    +from yt_dlp.downloader.external import (
    +    Aria2cFD,
    +    AxelFD,
    +    CurlFD,
    +    FFmpegFD,
    +    HttpieFD,
    +    WgetFD,
    +)
    +
    +TEST_COOKIE = {
    +    'version': 0,
    +    'name': 'test',
    +    'value': 'ytdlp',
    +    'port': None,
    +    'port_specified': False,
    +    'domain': '.example.com',
    +    'domain_specified': True,
    +    'domain_initial_dot': False,
    +    'path': '/',
    +    'path_specified': True,
    +    'secure': False,
    +    'expires': None,
    +    'discard': False,
    +    'comment': None,
    +    'comment_url': None,
    +    'rest': {},
    +}
    +
    +TEST_INFO = {'url': 'http://www.example.com/'}
    +
    +
    +class TestHttpieFD(unittest.TestCase):
    +    def test_make_cmd(self):
    +        with FakeYDL() as ydl:
    +            downloader = HttpieFD(ydl, {})
    +            self.assertEqual(
    +                downloader._make_cmd('test', TEST_INFO),
    +                ['http', '--download', '--output', 'test', 'http://www.example.com/'])
    +
    +            # Test cookie header is added
    +            ydl.cookiejar.set_cookie(http.cookiejar.Cookie(**TEST_COOKIE))
    +            self.assertEqual(
    +                downloader._make_cmd('test', TEST_INFO),
    +                ['http', '--download', '--output', 'test', 'http://www.example.com/', 'Cookie:test=ytdlp'])
    +
    +
    +class TestAxelFD(unittest.TestCase):
    +    def test_make_cmd(self):
    +        with FakeYDL() as ydl:
    +            downloader = AxelFD(ydl, {})
    +            self.assertEqual(
    +                downloader._make_cmd('test', TEST_INFO),
    +                ['axel', '-o', 'test', '--', 'http://www.example.com/'])
    +
    +            # Test cookie header is added
    +            ydl.cookiejar.set_cookie(http.cookiejar.Cookie(**TEST_COOKIE))
    +            self.assertEqual(
    +                downloader._make_cmd('test', TEST_INFO),
    +                ['axel', '-o', 'test', 'Cookie: test=ytdlp', '--max-redirect=0', '--', 'http://www.example.com/'])
    +
    +
    +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):
    +        with FakeYDL() as ydl:
    +            downloader = CurlFD(ydl, {})
    +            self.assertNotIn('--cookie-jar', downloader._make_cmd('test', TEST_INFO))
    +            # Test cookiejar tempfile arg is added
    +            ydl.cookiejar.set_cookie(http.cookiejar.Cookie(**TEST_COOKIE))
    +            self.assertIn('--cookie-jar', downloader._make_cmd('test', TEST_INFO))
    +
    +
    +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)
    +
    +
    +@unittest.skipUnless(FFmpegFD.available(), 'ffmpeg not found')
    +class TestFFmpegFD(unittest.TestCase):
    +    _args = []
    +
    +    def _test_cmd(self, args):
    +        self._args = args
    +
    +    def test_make_cmd(self):
    +        with FakeYDL() as ydl:
    +            downloader = FFmpegFD(ydl, {})
    +            downloader._debug_cmd = self._test_cmd
    +
    +            downloader._call_downloader('test', {**TEST_INFO, 'ext': 'mp4'})
    +            self.assertEqual(self._args, [
    +                'ffmpeg', '-y', '-hide_banner', '-i', 'http://www.example.com/',
    +                '-c', 'copy', '-f', 'mp4', 'file:test'])
    +
    +            # Test cookies arg is added
    +            ydl.cookiejar.set_cookie(http.cookiejar.Cookie(**TEST_COOKIE))
    +            downloader._call_downloader('test', {**TEST_INFO, 'ext': 'mp4'})
    +            self.assertEqual(self._args, [
    +                'ffmpeg', '-y', '-hide_banner', '-cookies', 'test=ytdlp; path=/; domain=.example.com;\r\n',
    +                '-i', 'http://www.example.com/', '-c', 'copy', '-f', 'mp4', 'file:test'])
    +
    +
    +if __name__ == '__main__':
    +    unittest.main()
    
  • yt_dlp/cookies.py+7 0 modified
    @@ -1327,6 +1327,13 @@ def get_cookie_header(self, url):
             self.add_cookie_header(cookie_req)
             return cookie_req.get_header('Cookie')
     
    +    def get_cookies_for_url(self, url):
    +        """Generate a list of Cookie objects for a given url"""
    +        # Policy `_now` attribute must be set before calling `_cookies_for_request`
    +        # Ref: https://github.com/python/cpython/blob/3.7/Lib/http/cookiejar.py#L1360
    +        self._policy._now = self._now = int(time.time())
    +        return self._cookies_for_request(urllib.request.Request(escape_url(sanitize_url(url))))
    +
         def clear(self, *args, **kwargs):
             with contextlib.suppress(KeyError):
                 return super().clear(*args, **kwargs)
    
  • yt_dlp/downloader/external.py+39 2 modified
    @@ -1,9 +1,10 @@
     import enum
     import json
    -import os.path
    +import os
     import re
     import subprocess
     import sys
    +import tempfile
     import time
     import uuid
     
    @@ -42,6 +43,7 @@ class ExternalFD(FragmentFD):
         def real_download(self, filename, info_dict):
             self.report_destination(filename)
             tmpfilename = self.temp_name(filename)
    +        self._cookies_tempfile = None
     
             try:
                 started = time.time()
    @@ -54,6 +56,9 @@ def real_download(self, filename, info_dict):
                 # should take place
                 retval = 0
                 self.to_screen('[%s] Interrupted by user' % self.get_basename())
    +        finally:
    +            if self._cookies_tempfile:
    +                self.try_remove(self._cookies_tempfile)
     
             if retval == 0:
                 status = {
    @@ -125,6 +130,16 @@ def _configuration_args(self, keys=None, *args, **kwargs):
                 self.get_basename(), self.params.get('external_downloader_args'), self.EXE_NAME,
                 keys, *args, **kwargs)
     
    +    def _write_cookies(self):
    +        if not self.ydl.cookiejar.filename:
    +            tmp_cookies = tempfile.NamedTemporaryFile(suffix='.cookies', delete=False)
    +            tmp_cookies.close()
    +            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)
    +        return self.ydl.cookiejar.filename or self._cookies_tempfile
    +
         def _call_downloader(self, tmpfilename, info_dict):
             """ Either overwrite this or implement _make_cmd """
             cmd = [encodeArgument(a) for a in self._make_cmd(tmpfilename, info_dict)]
    @@ -184,6 +199,8 @@ class CurlFD(ExternalFD):
     
         def _make_cmd(self, tmpfilename, info_dict):
             cmd = [self.exe, '--location', '-o', tmpfilename, '--compressed']
    +        if self.ydl.cookiejar.get_cookie_header(info_dict['url']):
    +            cmd += ['--cookie-jar', 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}']
    @@ -214,6 +231,9 @@ def _make_cmd(self, tmpfilename, info_dict):
             if info_dict.get('http_headers') is not None:
                 for key, val in info_dict['http_headers'].items():
                     cmd += ['-H', f'{key}: {val}']
    +        cookie_header = self.ydl.cookiejar.get_cookie_header(info_dict['url'])
    +        if cookie_header:
    +            cmd += [f'Cookie: {cookie_header}', '--max-redirect=0']
             cmd += self._configuration_args()
             cmd += ['--', info_dict['url']]
             return cmd
    @@ -223,7 +243,9 @@ class WgetFD(ExternalFD):
         AVAILABLE_OPT = '--version'
     
         def _make_cmd(self, tmpfilename, info_dict):
    -        cmd = [self.exe, '-O', tmpfilename, '-nv', '--no-cookies', '--compression=auto']
    +        cmd = [self.exe, '-O', tmpfilename, '-nv', '--compression=auto']
    +        if self.ydl.cookiejar.get_cookie_header(info_dict['url']):
    +            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}']
    @@ -279,6 +301,8 @@ 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()}']
             if info_dict.get('http_headers') is not None:
                 for key, val in info_dict['http_headers'].items():
                     cmd += ['--header', f'{key}: {val}']
    @@ -417,6 +441,14 @@ def _make_cmd(self, tmpfilename, info_dict):
             if info_dict.get('http_headers') is not None:
                 for key, val in info_dict['http_headers'].items():
                     cmd += [f'{key}:{val}']
    +
    +        # httpie 3.1.0+ removes the Cookie header on redirect, so this should be safe for now. [1]
    +        # If we ever need cookie handling for redirects, we can export the cookiejar into a session. [2]
    +        # 1: https://github.com/httpie/httpie/security/advisories/GHSA-9w4w-cpc8-h2fq
    +        # 2: https://httpie.io/docs/cli/sessions
    +        cookie_header = self.ydl.cookiejar.get_cookie_header(info_dict['url'])
    +        if cookie_header:
    +            cmd += [f'Cookie:{cookie_header}']
             return cmd
     
     
    @@ -527,6 +559,11 @@ def _call_downloader(self, tmpfilename, info_dict):
     
             selected_formats = info_dict.get('requested_formats') or [info_dict]
             for i, fmt in enumerate(selected_formats):
    +            cookies = self.ydl.cookiejar.get_cookies_for_url(fmt['url'])
    +            if cookies:
    +                args.extend(['-cookies', ''.join(
    +                    f'{cookie.name}={cookie.value}; path={cookie.path}; domain={cookie.domain};\r\n'
    +                    for cookie in cookies)])
                 if fmt.get('http_headers') and re.match(r'^https?://', fmt['url']):
                     # Trailing \r\n after each HTTP header is important to prevent warning from ffmpeg/avconv:
                     # [http @ 00000000003d2fa0] No trailing CRLF found in HTTP header.
    
f8b4bcc0a791

[core] Prevent `Cookie` leaks on HTTP redirect

https://github.com/yt-dlp/yt-dlpcoletdjnzJun 6, 2023via ghsa
2 files changed · +38 2
  • test/test_http.py+31 0 modified
    @@ -132,6 +132,11 @@ def do_GET(self):
                 self._method('GET')
             elif self.path.startswith('/headers'):
                 self._headers()
    +        elif self.path.startswith('/308-to-headers'):
    +            self.send_response(308)
    +            self.send_header('Location', '/headers')
    +            self.send_header('Content-Length', '0')
    +            self.end_headers()
             elif self.path == '/trailing_garbage':
                 payload = b'<html><video src="/vid.mp4" /></html>'
                 self.send_response(200)
    @@ -270,6 +275,7 @@ def do_req(redirect_status, method):
                 self.assertEqual(do_req(303, 'PUT'), ('', 'GET'))
     
                 # 301 and 302 turn POST only into a GET
    +            # XXX: we should also test if the Content-Type and Content-Length headers are removed
                 self.assertEqual(do_req(301, 'POST'), ('', 'GET'))
                 self.assertEqual(do_req(301, 'HEAD'), ('', 'HEAD'))
                 self.assertEqual(do_req(302, 'POST'), ('', 'GET'))
    @@ -313,6 +319,31 @@ def test_cookiejar(self):
                 data = ydl.urlopen(sanitized_Request(f'http://127.0.0.1:{self.http_port}/headers')).read()
                 self.assertIn(b'Cookie: test=ytdlp', data)
     
    +    def test_passed_cookie_header(self):
    +        # We should accept a Cookie header being passed as in normal headers and handle it appropriately.
    +        with FakeYDL() as ydl:
    +            # Specified Cookie header should be used
    +            res = ydl.urlopen(
    +                sanitized_Request(f'http://127.0.0.1:{self.http_port}/headers',
    +                                  headers={'Cookie': 'test=test'})).read().decode('utf-8')
    +            self.assertIn('Cookie: test=test', res)
    +
    +            # Specified Cookie header should be removed on any redirect
    +            res = ydl.urlopen(
    +                sanitized_Request(f'http://127.0.0.1:{self.http_port}/308-to-headers', headers={'Cookie': 'test=test'})).read().decode('utf-8')
    +            self.assertNotIn('Cookie: test=test', res)
    +
    +            # Specified Cookie header should override global cookiejar for that request
    +            ydl.cookiejar.set_cookie(http.cookiejar.Cookie(
    +                version=0, name='test', value='ytdlp', port=None, port_specified=False,
    +                domain='127.0.0.1', domain_specified=True, domain_initial_dot=False, path='/',
    +                path_specified=True, secure=False, expires=None, discard=False, comment=None,
    +                comment_url=None, rest={}))
    +
    +            data = ydl.urlopen(sanitized_Request(f'http://127.0.0.1:{self.http_port}/headers', headers={'Cookie': 'test=test'})).read()
    +            self.assertNotIn(b'Cookie: test=ytdlp', data)
    +            self.assertIn(b'Cookie: test=test', data)
    +
         def test_no_compression_compat_header(self):
             with FakeYDL() as ydl:
                 data = ydl.urlopen(
    
  • yt_dlp/utils/_utils.py+7 2 modified
    @@ -1556,7 +1556,12 @@ def redirect_request(self, req, fp, code, msg, headers, newurl):
     
             new_method = req.get_method()
             new_data = req.data
    -        remove_headers = []
    +
    +        # Technically the Cookie header should be in unredirected_hdrs,
    +        # however in practice some may set it in normal headers anyway.
    +        # We will remove it here to prevent any leaks.
    +        remove_headers = ['Cookie']
    +
             # A 303 must either use GET or HEAD for subsequent request
             # https://datatracker.ietf.org/doc/html/rfc7231#section-6.4.4
             if code == 303 and req.get_method() != 'HEAD':
    @@ -1573,7 +1578,7 @@ def redirect_request(self, req, fp, code, msg, headers, newurl):
                 new_data = None
                 remove_headers.extend(['Content-Length', 'Content-Type'])
     
    -        new_headers = {k: v for k, v in req.headers.items() if k.lower() not in remove_headers}
    +        new_headers = {k: v for k, v in req.headers.items() if k.title() not in remove_headers}
     
             return urllib.request.Request(
                 newurl, headers=new_headers, origin_req_host=req.origin_req_host,
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

16

News mentions

0

No linked articles in our index yet.