CVE-2024-38519 (Python, published 2024-07-02T14:15Z)
Description: value: `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.
Severity: nan (CVSS v3 base score: )
Weakness: NVD-CWE-noinfo \u2014 Insufficient Information
There is insufficient information about the issue to classify it; details are unkown or unspecified.
Repository: https://github.com/yt-dlp/yt-dlp
Files changed:
README.md
devscripts/changelog_override.json
test/test_utils.py
yt_dlp/YoutubeDL.py
yt_dlp/__init__.py
yt_dlp/options.py
yt_dlp/utils/_utils.py
Commit message: [core] Disallow unsafe extensions (CVE-2024-38519)

Ref: https://github.com/yt-dlp/yt-dlp/security/advisories/GHSA-79w7-vh3h-8g4j

Authored by: Grub4K


Vulnerable code:
from .utils._utils import _YDLLogger
                       filename, abort-on-error, multistreams, no-live-chat, format-sort
                       no-clean-infojson, no-playlist-metafiles, no-keep-subs, no-attach-info-json.
                'prefer-legacy-http-handler', 'manifest-filesize-approx',
def prepend_extension(filename, ext, expected_real_ext=None):
    return (
        f'{name}.{ext}{real_ext}'
        if not expected_real_ext or real_ext[1:] == expected_real_ext
        else f'{filename}.{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}'

Fixed code:
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!**

    },
    {
        "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"
from yt_dlp.utils._utils import _UnsafeExtensionError
    _uncommon_extensions = [
        ('exe', 'abc.exe.ext'),
        ('de', 'abc.de.ext'),
        ('../.mp4', None),
        ('..\\.mp4', None),
    ]

        # 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')

        # 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')

from .utils._utils import _UnsafeExtensionError, _YDLLogger
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


                       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.
    @_catch_unsafe_extension_error
    @_catch_unsafe_extension_error
from .utils._utils import _UnsafeExtensionError
    # 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

                'prefer-legacy-http-handler', 'manifest-filesize-approx', 'allow-unsafe-ext',
def _change_extension(prepend, filename, ext, expected_real_ext=None):
    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)}'

prepend_extension = functools.partial(_change_extension, True)
replace_extension = functools.partial(_change_extension, False)
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