VYPR
High severity7.8NVD Advisory· Published Jul 2, 2024· Updated Apr 15, 2026

CVE-2024-38519

CVE-2024-38519

Description

yt-dlp and youtube-dl are command-line audio/video downloaders. Prior to the fixed versions, yt-dlp and youtube-dl do not limit the extensions of downloaded files, which could lead to arbitrary filenames being created in the download folder (and path traversal on Windows). Since yt-dlp and youtube-dl also read config from the working directory (and on Windows executables will be executed from the yt-dlp or youtube-dl directory), this could lead to arbitrary code being executed.

yt-dlp version 2024.07.01 fixes this issue by whitelisting the allowed extensions. youtube-dl fixes this issue in commit d42a222 on the master branch and in nightly builds tagged 2024-07-03 or later. This might mean some very uncommon extensions might not get downloaded, however it will also limit the possible exploitation surface. In addition to upgrading, have .%(ext)s at the end of the output template and make sure the user trusts the websites that they are downloading from. Also, make sure to never download to a directory within PATH or other sensitive locations like one's user directory, system32, or other binaries locations. For users who are not able to upgrade, keep the default output template (-o "%(title)s [%(id)s].%(ext)s); make sure the extension of the media to download is a common video/audio/sub/... one; try to avoid the generic extractor; and/or use --ignore-config --config-location ... to not load config from common locations.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
yt-dlpPyPI
< 2024.07.012024.07.01

Patches

3
5ce582448ece

[core] Disallow unsafe extensions (CVE-2024-38519)

https://github.com/yt-dlp/yt-dlpSimon SawickiJul 1, 2024via ghsa
7 files changed · +179 12
  • devscripts/changelog_override.json+5 0 modified
    @@ -175,5 +175,10 @@
             "when": "e6a22834df1776ec4e486526f6df2bf53cb7e06f",
             "short": "[ie/orf:on] Add `prefer_segments_playlist` extractor-arg (#10314)",
             "authors": ["seproDev"]
    +    },
    +    {
    +        "action": "add",
    +        "when": "6aaf96a3d6e7d0d426e97e11a2fcf52fda00e733",
    +        "short": "[priority] Security: [[CVE-2024-10123](https://nvd.nist.gov/vuln/detail/CVE-2024-10123)] [Properly sanitize file-extension to prevent file system modification and RCE](https://github.com/yt-dlp/yt-dlp/security/advisories/GHSA-79w7-vh3h-8g4j)\n    - Unsafe extensions are now blocked from being downloaded"
         }
     ]
    
  • README.md+8 0 modified
    @@ -2229,6 +2229,14 @@ For ease of use, a few more compat options are available:
     * `--compat-options 2022`: Same as `--compat-options 2023,playlist-match-filter,no-external-downloader-progress,prefer-legacy-http-handler,manifest-filesize-approx`
     * `--compat-options 2023`: Currently does nothing. Use this to enable all future compat options
     
    +The following compat options restore vulnerable behavior from before security patches:
    +
    +* `--compat-options allow-unsafe-ext`: Allow files with any extension (including unsafe ones) to be downloaded ([GHSA-79w7-vh3h-8g4j](<https://github.com/yt-dlp/yt-dlp/security/advisories/GHSA-79w7-vh3h-8g4j>))
    +
    +    > :warning: Only use if a valid file download is rejected because its extension is detected as uncommon
    +    >
    +    > **This option can enable remote code execution! Consider [opening an issue](<https://github.com/yt-dlp/yt-dlp/issues/new/choose>) instead!**
    +
     ### Deprecated options
     
     These are all the deprecated options and the current alternative to achieve the same effect
    
  • test/test_utils.py+31 0 modified
    @@ -130,6 +130,7 @@
         xpath_text,
         xpath_with_ns,
     )
    +from yt_dlp.utils._utils import _UnsafeExtensionError
     from yt_dlp.utils.networking import (
         HTTPHeaderDict,
         escape_rfc3986,
    @@ -281,6 +282,13 @@ def env(var):
             finally:
                 os.environ['HOME'] = old_home or ''
     
    +    _uncommon_extensions = [
    +        ('exe', 'abc.exe.ext'),
    +        ('de', 'abc.de.ext'),
    +        ('../.mp4', None),
    +        ('..\\.mp4', None),
    +    ]
    +
         def test_prepend_extension(self):
             self.assertEqual(prepend_extension('abc.ext', 'temp'), 'abc.temp.ext')
             self.assertEqual(prepend_extension('abc.ext', 'temp', 'ext'), 'abc.temp.ext')
    @@ -289,6 +297,19 @@ def test_prepend_extension(self):
             self.assertEqual(prepend_extension('.abc', 'temp'), '.abc.temp')
             self.assertEqual(prepend_extension('.abc.ext', 'temp'), '.abc.temp.ext')
     
    +        # Test uncommon extensions
    +        self.assertEqual(prepend_extension('abc.ext', 'bin'), 'abc.bin.ext')
    +        for ext, result in self._uncommon_extensions:
    +            with self.assertRaises(_UnsafeExtensionError):
    +                prepend_extension('abc', ext)
    +            if result:
    +                self.assertEqual(prepend_extension('abc.ext', ext, 'ext'), result)
    +            else:
    +                with self.assertRaises(_UnsafeExtensionError):
    +                    prepend_extension('abc.ext', ext, 'ext')
    +            with self.assertRaises(_UnsafeExtensionError):
    +                prepend_extension('abc.unexpected_ext', ext, 'ext')
    +
         def test_replace_extension(self):
             self.assertEqual(replace_extension('abc.ext', 'temp'), 'abc.temp')
             self.assertEqual(replace_extension('abc.ext', 'temp', 'ext'), 'abc.temp')
    @@ -297,6 +318,16 @@ def test_replace_extension(self):
             self.assertEqual(replace_extension('.abc', 'temp'), '.abc.temp')
             self.assertEqual(replace_extension('.abc.ext', 'temp'), '.abc.temp')
     
    +        # Test uncommon extensions
    +        self.assertEqual(replace_extension('abc.ext', 'bin'), 'abc.unknown_video')
    +        for ext, _ in self._uncommon_extensions:
    +            with self.assertRaises(_UnsafeExtensionError):
    +                replace_extension('abc', ext)
    +            with self.assertRaises(_UnsafeExtensionError):
    +                replace_extension('abc.ext', ext, 'ext')
    +            with self.assertRaises(_UnsafeExtensionError):
    +                replace_extension('abc.unexpected_ext', ext, 'ext')
    +
         def test_subtitles_filename(self):
             self.assertEqual(subtitles_filename('abc.ext', 'en', 'vtt'), 'abc.en.vtt')
             self.assertEqual(subtitles_filename('abc.ext', 'en', 'vtt', 'ext'), 'abc.en.vtt')
    
  • yt_dlp/__init__.py+8 0 modified
    @@ -64,6 +64,7 @@
         write_string,
     )
     from .utils.networking import std_headers
    +from .utils._utils import _UnsafeExtensionError
     from .YoutubeDL import YoutubeDL
     
     _IN_CLI = False
    @@ -593,6 +594,13 @@ def report_deprecation(val, old, new=None):
         if opts.ap_username is not None and opts.ap_password is None:
             opts.ap_password = getpass.getpass('Type TV provider account password and press [Return]: ')
     
    +    # compat option changes global state destructively; only allow from cli
    +    if 'allow-unsafe-ext' in opts.compat_opts:
    +        warnings.append(
    +            'Using allow-unsafe-ext opens you up to potential attacks. '
    +            'Use with great care!')
    +        _UnsafeExtensionError.sanitize_extension = lambda x: x
    +
         return warnings, deprecation_warnings
     
     
    
  • yt_dlp/options.py+1 1 modified
    @@ -474,7 +474,7 @@ def _alias_callback(option, opt_str, value, parser, opts, nargs):
                     'no-attach-info-json', 'embed-thumbnail-atomicparsley', 'no-external-downloader-progress',
                     'embed-metadata', 'seperate-video-versions', 'no-clean-infojson', 'no-keep-subs', 'no-certifi',
                     'no-youtube-channel-redirect', 'no-youtube-unavailable-videos', 'no-youtube-prefer-utc-upload-date',
    -                'prefer-legacy-http-handler', 'manifest-filesize-approx',
    +                'prefer-legacy-http-handler', 'manifest-filesize-approx', 'allow-unsafe-ext',
                 }, 'aliases': {
                     'youtube-dl': ['all', '-multistreams', '-playlist-match-filter', '-manifest-filesize-approx'],
                     'youtube-dlc': ['all', '-no-youtube-channel-redirect', '-no-live-chat', '-playlist-match-filter', '-manifest-filesize-approx'],
    
  • yt_dlp/utils/_utils.py+106 8 modified
    @@ -2085,17 +2085,20 @@ def parse_duration(s):
             (days, 86400), (hours, 3600), (mins, 60), (secs, 1), (ms, 1)))
     
     
    -def prepend_extension(filename, ext, expected_real_ext=None):
    +def _change_extension(prepend, filename, ext, expected_real_ext=None):
         name, real_ext = os.path.splitext(filename)
    -    return (
    -        f'{name}.{ext}{real_ext}'
    -        if not expected_real_ext or real_ext[1:] == expected_real_ext
    -        else f'{filename}.{ext}')
     
    +    if not expected_real_ext or real_ext[1:] == expected_real_ext:
    +        filename = name
    +        if prepend and real_ext:
    +            _UnsafeExtensionError.sanitize_extension(ext, prepend=True)
    +            return f'{filename}.{ext}{real_ext}'
    +
    +    return f'{filename}.{_UnsafeExtensionError.sanitize_extension(ext)}'
     
    -def replace_extension(filename, ext, expected_real_ext=None):
    -    name, real_ext = os.path.splitext(filename)
    -    return f'{name if not expected_real_ext or real_ext[1:] == expected_real_ext else filename}.{ext}'
    +
    +prepend_extension = functools.partial(_change_extension, True)
    +replace_extension = functools.partial(_change_extension, False)
     
     
     def check_executable(exe, args=[]):
    @@ -5035,6 +5038,101 @@ def items_(self):
     KNOWN_EXTENSIONS = (*MEDIA_EXTENSIONS.video, *MEDIA_EXTENSIONS.audio, *MEDIA_EXTENSIONS.manifests)
     
     
    +class _UnsafeExtensionError(Exception):
    +    """
    +    Mitigation exception for uncommon/malicious file extensions
    +    This should be caught in YoutubeDL.py alongside a warning
    +
    +    Ref: https://github.com/yt-dlp/yt-dlp/security/advisories/GHSA-79w7-vh3h-8g4j
    +    """
    +    ALLOWED_EXTENSIONS = frozenset([
    +        # internal
    +        'description',
    +        'json',
    +        'meta',
    +        'orig',
    +        'part',
    +        'temp',
    +        'uncut',
    +        'unknown_video',
    +        'ytdl',
    +
    +        # video
    +        *MEDIA_EXTENSIONS.video,
    +        'avif',
    +        'ismv',
    +        'm2ts',
    +        'm4s',
    +        'mng',
    +        'mpeg',
    +        'qt',
    +        'swf',
    +        'ts',
    +        'vp9',
    +        'wvm',
    +
    +        # audio
    +        *MEDIA_EXTENSIONS.audio,
    +        'isma',
    +        'mid',
    +        'mpga',
    +        'ra',
    +
    +        # image
    +        *MEDIA_EXTENSIONS.thumbnails,
    +        'bmp',
    +        'gif',
    +        'heic',
    +        'ico',
    +        'jng',
    +        'jpeg',
    +        'jxl',
    +        'svg',
    +        'tif',
    +        'wbmp',
    +
    +        # subtitle
    +        *MEDIA_EXTENSIONS.subtitles,
    +        'dfxp',
    +        'fs',
    +        'ismt',
    +        'sami',
    +        'scc',
    +        'ssa',
    +        'tt',
    +        'ttml',
    +
    +        # others
    +        *MEDIA_EXTENSIONS.manifests,
    +        *MEDIA_EXTENSIONS.storyboards,
    +        'desktop',
    +        'ism',
    +        'm3u',
    +        'sbv',
    +        'url',
    +        'webloc',
    +        'xml',
    +    ])
    +
    +    def __init__(self, extension, /):
    +        super().__init__(f'unsafe file extension: {extension!r}')
    +        self.extension = extension
    +
    +    @classmethod
    +    def sanitize_extension(cls, extension, /, *, prepend=False):
    +        if '/' in extension or '\\' in extension:
    +            raise cls(extension)
    +
    +        if not prepend:
    +            _, _, last = extension.rpartition('.')
    +            if last == 'bin':
    +                extension = last = 'unknown_video'
    +            if last.lower() not in cls.ALLOWED_EXTENSIONS:
    +                raise cls(extension)
    +
    +        return extension
    +
    +
     class RetryManager:
         """Usage:
             for retry in RetryManager(...):
    
  • yt_dlp/YoutubeDL.py+20 3 modified
    @@ -159,7 +159,7 @@
         write_json_file,
         write_string,
     )
    -from .utils._utils import _YDLLogger
    +from .utils._utils import _UnsafeExtensionError, _YDLLogger
     from .utils.networking import (
         HTTPHeaderDict,
         clean_headers,
    @@ -172,6 +172,20 @@
         import ctypes
     
     
    +def _catch_unsafe_extension_error(func):
    +    @functools.wraps(func)
    +    def wrapper(self, *args, **kwargs):
    +        try:
    +            return func(self, *args, **kwargs)
    +        except _UnsafeExtensionError as error:
    +            self.report_error(
    +                f'The extracted extension ({error.extension!r}) is unusual '
    +                'and will be skipped for safety reasons. '
    +                f'If you believe this is an error{bug_reports_message(",")}')
    +
    +    return wrapper
    +
    +
     class YoutubeDL:
         """YoutubeDL class.
     
    @@ -454,8 +468,9 @@ class YoutubeDL:
                            Set the value to 'native' to use the native downloader
         compat_opts:       Compatibility options. See "Differences in default behavior".
                            The following options do not work when used through the API:
    -                       filename, abort-on-error, multistreams, no-live-chat, format-sort
    -                       no-clean-infojson, no-playlist-metafiles, no-keep-subs, no-attach-info-json.
    +                       filename, abort-on-error, multistreams, no-live-chat,
    +                       format-sort, no-clean-infojson, no-playlist-metafiles,
    +                       no-keep-subs, no-attach-info-json, allow-unsafe-ext.
                            Refer __init__.py for their implementation
         progress_template: Dictionary of templates for progress outputs.
                            Allowed keys are 'download', 'postprocess',
    @@ -1400,6 +1415,7 @@ def evaluate_outtmpl(self, outtmpl, info_dict, *args, **kwargs):
             outtmpl, info_dict = self.prepare_outtmpl(outtmpl, info_dict, *args, **kwargs)
             return self.escape_outtmpl(outtmpl) % info_dict
     
    +    @_catch_unsafe_extension_error
         def _prepare_filename(self, info_dict, *, outtmpl=None, tmpl_type=None):
             assert None in (outtmpl, tmpl_type), 'outtmpl and tmpl_type are mutually exclusive'
             if outtmpl is None:
    @@ -3192,6 +3208,7 @@ def existing_file(self, filepaths, *, default_overwrite=True):
                 os.remove(file)
             return None
     
    +    @_catch_unsafe_extension_error
         def process_info(self, info_dict):
             """Process a single resolved IE result. (Modifies it in-place)"""
     
    
d42a222ed541

[core,utils] Implement unsafe file extension mitigation

3 files changed · +209 43
  • test/test_utils.py+46 0 modified
    @@ -14,9 +14,11 @@
     import io
     import itertools
     import json
    +import types
     import xml.etree.ElementTree
     
     from youtube_dl.utils import (
    +    _UnsafeExtensionError,
         age_restricted,
         args_to_str,
         base_url,
    @@ -270,6 +272,27 @@ def env(var):
                 expand_path('~/%s' % env('YOUTUBE_DL_EXPATH_PATH')),
                 '%s/expanded' % compat_getenv('HOME'))
     
    +    _uncommon_extensions = [
    +        ('exe', 'abc.exe.ext'),
    +        ('de', 'abc.de.ext'),
    +        ('../.mp4', None),
    +        ('..\\.mp4', None),
    +    ]
    +
    +    def assertUnsafeExtension(self, ext=None):
    +        assert_raises = self.assertRaises(_UnsafeExtensionError)
    +        assert_raises.ext = ext
    +        orig_exit = assert_raises.__exit__
    +
    +        def my_exit(self_, exc_type, exc_val, exc_tb):
    +            did_raise = orig_exit(exc_type, exc_val, exc_tb)
    +            if did_raise and assert_raises.ext is not None:
    +                self.assertEqual(assert_raises.ext, assert_raises.exception.extension, 'Unsafe extension  not as unexpected')
    +            return did_raise
    +
    +        assert_raises.__exit__ = types.MethodType(my_exit, assert_raises)
    +        return assert_raises
    +
         def test_prepend_extension(self):
             self.assertEqual(prepend_extension('abc.ext', 'temp'), 'abc.temp.ext')
             self.assertEqual(prepend_extension('abc.ext', 'temp', 'ext'), 'abc.temp.ext')
    @@ -278,6 +301,19 @@ def test_prepend_extension(self):
             self.assertEqual(prepend_extension('.abc', 'temp'), '.abc.temp')
             self.assertEqual(prepend_extension('.abc.ext', 'temp'), '.abc.temp.ext')
     
    +        # Test uncommon extensions
    +        self.assertEqual(prepend_extension('abc.ext', 'bin'), 'abc.bin.ext')
    +        for ext, result in self._uncommon_extensions:
    +            with self.assertUnsafeExtension(ext):
    +                prepend_extension('abc', ext)
    +            if result:
    +                self.assertEqual(prepend_extension('abc.ext', ext, 'ext'), result)
    +            else:
    +                with self.assertUnsafeExtension(ext):
    +                    prepend_extension('abc.ext', ext, 'ext')
    +            with self.assertUnsafeExtension(ext):
    +                prepend_extension('abc.unexpected_ext', ext, 'ext')
    +
         def test_replace_extension(self):
             self.assertEqual(replace_extension('abc.ext', 'temp'), 'abc.temp')
             self.assertEqual(replace_extension('abc.ext', 'temp', 'ext'), 'abc.temp')
    @@ -286,6 +322,16 @@ def test_replace_extension(self):
             self.assertEqual(replace_extension('.abc', 'temp'), '.abc.temp')
             self.assertEqual(replace_extension('.abc.ext', 'temp'), '.abc.temp')
     
    +        # Test uncommon extensions
    +        self.assertEqual(replace_extension('abc.ext', 'bin'), 'abc.unknown_video')
    +        for ext, _ in self._uncommon_extensions:
    +            with self.assertUnsafeExtension(ext):
    +                replace_extension('abc', ext)
    +            with self.assertUnsafeExtension(ext):
    +                replace_extension('abc.ext', ext, 'ext')
    +            with self.assertUnsafeExtension(ext):
    +                replace_extension('abc.unexpected_ext', ext, 'ext')
    +
         def test_subtitles_filename(self):
             self.assertEqual(subtitles_filename('abc.ext', 'en', 'vtt'), 'abc.en.vtt')
             self.assertEqual(subtitles_filename('abc.ext', 'en', 'vtt', 'ext'), 'abc.en.vtt')
    
  • youtube_dl/utils.py+146 43 modified
    @@ -1717,39 +1717,6 @@ def random_user_agent():
         'PST': -8, 'PDT': -7   # Pacific
     }
     
    -
    -class Namespace(object):
    -    """Immutable namespace"""
    -
    -    def __init__(self, **kw_attr):
    -        self.__dict__.update(kw_attr)
    -
    -    def __iter__(self):
    -        return iter(self.__dict__.values())
    -
    -    @property
    -    def items_(self):
    -        return self.__dict__.items()
    -
    -
    -MEDIA_EXTENSIONS = Namespace(
    -    common_video=('avi', 'flv', 'mkv', 'mov', 'mp4', 'webm'),
    -    video=('3g2', '3gp', 'f4v', 'mk3d', 'divx', 'mpg', 'ogv', 'm4v', 'wmv'),
    -    common_audio=('aiff', 'alac', 'flac', 'm4a', 'mka', 'mp3', 'ogg', 'opus', 'wav'),
    -    audio=('aac', 'ape', 'asf', 'f4a', 'f4b', 'm4b', 'm4p', 'm4r', 'oga', 'ogx', 'spx', 'vorbis', 'wma', 'weba'),
    -    thumbnails=('jpg', 'png', 'webp'),
    -    # storyboards=('mhtml', ),
    -    subtitles=('srt', 'vtt', 'ass', 'lrc', 'ttml'),
    -    manifests=('f4f', 'f4m', 'm3u8', 'smil', 'mpd'),
    -)
    -MEDIA_EXTENSIONS.video = MEDIA_EXTENSIONS.common_video + MEDIA_EXTENSIONS.video
    -MEDIA_EXTENSIONS.audio = MEDIA_EXTENSIONS.common_audio + MEDIA_EXTENSIONS.audio
    -
    -KNOWN_EXTENSIONS = (
    -    MEDIA_EXTENSIONS.video + MEDIA_EXTENSIONS.audio
    -    + MEDIA_EXTENSIONS.manifests
    -)
    -
     # needed for sanitizing filenames in restricted mode
     ACCENT_CHARS = dict(zip('ÂÃÄÀÁÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖŐØŒÙÚÛÜŰÝÞßàáâãäåæçèéêëìíîïðñòóôõöőøœùúûüűýþÿ',
                             itertools.chain('AAAAAA', ['AE'], 'CEEEEIIIIDNOOOOOOO', ['OE'], 'UUUUUY', ['TH', 'ss'],
    @@ -3977,19 +3944,22 @@ def parse_duration(s):
         return duration
     
     
    -def prepend_extension(filename, ext, expected_real_ext=None):
    +def _change_extension(prepend, filename, ext, expected_real_ext=None):
         name, real_ext = os.path.splitext(filename)
    -    return (
    -        '{0}.{1}{2}'.format(name, ext, real_ext)
    -        if not expected_real_ext or real_ext[1:] == expected_real_ext
    -        else '{0}.{1}'.format(filename, ext))
    +    sanitize_extension = _UnsafeExtensionError.sanitize_extension
     
    +    if not expected_real_ext or real_ext.partition('.')[0::2] == ('', expected_real_ext):
    +        filename = name
    +        if prepend and real_ext:
    +            sanitize_extension(ext, prepend=prepend)
    +            return ''.join((filename, '.', ext, real_ext))
     
    -def replace_extension(filename, ext, expected_real_ext=None):
    -    name, real_ext = os.path.splitext(filename)
    -    return '{0}.{1}'.format(
    -        name if not expected_real_ext or real_ext[1:] == expected_real_ext else filename,
    -        ext)
    +    # Mitigate path traversal and file impersonation attacks
    +    return '.'.join((filename, sanitize_extension(ext)))
    +
    +
    +prepend_extension = functools.partial(_change_extension, True)
    +replace_extension = functools.partial(_change_extension, False)
     
     
     def check_executable(exe, args=[]):
    @@ -6579,3 +6549,136 @@ def join_nonempty(*values, **kwargs):
         if from_dict is not None:
             values = (traverse_obj(from_dict, variadic(v)) for v in values)
         return delim.join(map(compat_str, filter(None, values)))
    +
    +
    +class Namespace(object):
    +    """Immutable namespace"""
    +
    +    def __init__(self, **kw_attr):
    +        self.__dict__.update(kw_attr)
    +
    +    def __iter__(self):
    +        return iter(self.__dict__.values())
    +
    +    @property
    +    def items_(self):
    +        return self.__dict__.items()
    +
    +
    +MEDIA_EXTENSIONS = Namespace(
    +    common_video=('avi', 'flv', 'mkv', 'mov', 'mp4', 'webm'),
    +    video=('3g2', '3gp', 'f4v', 'mk3d', 'divx', 'mpg', 'ogv', 'm4v', 'wmv'),
    +    common_audio=('aiff', 'alac', 'flac', 'm4a', 'mka', 'mp3', 'ogg', 'opus', 'wav'),
    +    audio=('aac', 'ape', 'asf', 'f4a', 'f4b', 'm4b', 'm4p', 'm4r', 'oga', 'ogx', 'spx', 'vorbis', 'wma', 'weba'),
    +    thumbnails=('jpg', 'png', 'webp'),
    +    # storyboards=('mhtml', ),
    +    subtitles=('srt', 'vtt', 'ass', 'lrc', 'ttml'),
    +    manifests=('f4f', 'f4m', 'm3u8', 'smil', 'mpd'),
    +)
    +MEDIA_EXTENSIONS.video = MEDIA_EXTENSIONS.common_video + MEDIA_EXTENSIONS.video
    +MEDIA_EXTENSIONS.audio = MEDIA_EXTENSIONS.common_audio + MEDIA_EXTENSIONS.audio
    +
    +KNOWN_EXTENSIONS = (
    +    MEDIA_EXTENSIONS.video + MEDIA_EXTENSIONS.audio
    +    + MEDIA_EXTENSIONS.manifests
    +)
    +
    +
    +class _UnsafeExtensionError(Exception):
    +    """
    +    Mitigation exception for unwanted file overwrite/path traversal
    +    This should be caught in YoutubeDL.py with a warning
    +
    +    Ref: https://github.com/yt-dlp/yt-dlp/security/advisories/GHSA-79w7-vh3h-8g4j
    +    """
    +    _ALLOWED_EXTENSIONS = frozenset(itertools.chain(
    +        (   # internal
    +            'description',
    +            'json',
    +            'meta',
    +            'orig',
    +            'part',
    +            'temp',
    +            'uncut',
    +            'unknown_video',
    +            'ytdl',
    +        ),
    +        # video
    +        MEDIA_EXTENSIONS.video, (
    +            'avif',
    +            'ismv',
    +            'm2ts',
    +            'm4s',
    +            'mng',
    +            'mpeg',
    +            'qt',
    +            'swf',
    +            'ts',
    +            'vp9',
    +            'wvm',
    +        ),
    +        # audio
    +        MEDIA_EXTENSIONS.audio, (
    +            'isma',
    +            'mid',
    +            'mpga',
    +            'ra',
    +        ),
    +        # image
    +        MEDIA_EXTENSIONS.thumbnails, (
    +            'bmp',
    +            'gif',
    +            'ico',
    +            'heic',
    +            'jng',
    +            'jpeg',
    +            'jxl',
    +            'svg',
    +            'tif',
    +            'wbmp',
    +        ),
    +        # subtitle
    +        MEDIA_EXTENSIONS.subtitles, (
    +            'dfxp',
    +            'fs',
    +            'ismt',
    +            'sami',
    +            'scc',
    +            'ssa',
    +            'tt',
    +        ),
    +        # others
    +        MEDIA_EXTENSIONS.manifests,
    +        (
    +            # not used in yt-dl
    +            # *MEDIA_EXTENSIONS.storyboards,
    +            # 'desktop',
    +            # 'ism',
    +            # 'm3u',
    +            # 'sbv',
    +            # 'swp',
    +            # 'url',
    +            # 'webloc',
    +            # 'xml',
    +        )))
    +
    +    def __init__(self, extension):
    +        super(_UnsafeExtensionError, self).__init__('unsafe file extension: {0!r}'.format(extension))
    +        self.extension = extension
    +
    +    @classmethod
    +    def sanitize_extension(cls, extension, **kwargs):
    +        # ... /, *, prepend=False
    +        prepend = kwargs.get('prepend', False)
    +
    +        if '/' in extension or '\\' in extension:
    +            raise cls(extension)
    +
    +        if not prepend:
    +            last = extension.rpartition('.')[-1]
    +            if last == 'bin':
    +                extension = last = 'unknown_video'
    +            if last.lower() not in cls._ALLOWED_EXTENSIONS:
    +                raise cls(extension)
    +
    +        return extension
    
  • youtube_dl/YoutubeDL.py+17 0 modified
    @@ -7,6 +7,7 @@
     import copy
     import datetime
     import errno
    +import functools
     import io
     import itertools
     import json
    @@ -53,6 +54,7 @@
         compat_urllib_request_DataHandler,
     )
     from .utils import (
    +    _UnsafeExtensionError,
         age_restricted,
         args_to_str,
         bug_reports_message,
    @@ -129,6 +131,20 @@
         import ctypes
     
     
    +def _catch_unsafe_file_extension(func):
    +    @functools.wraps(func)
    +    def wrapper(self, *args, **kwargs):
    +        try:
    +            return func(self, *args, **kwargs)
    +        except _UnsafeExtensionError as error:
    +            self.report_error(
    +                '{0} found; to avoid damaging your system, this value is disallowed.'
    +                ' If you believe this is an error{1}').format(
    +                    error.message, bug_reports_message(','))
    +
    +    return wrapper
    +
    +
     class YoutubeDL(object):
         """YoutubeDL class.
     
    @@ -1925,6 +1941,7 @@ def print_optional(field):
             if self.params.get('forcejson', False):
                 self.to_stdout(json.dumps(self.sanitize_info(info_dict)))
     
    +    @_catch_unsafe_file_extension
         def process_info(self, info_dict):
             """Process a single resolved IE result."""
     
    

Vulnerability mechanics

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

References

11

News mentions

0

No linked articles in our index yet.