aboutsummaryrefslogtreecommitdiff
path: root/yt_dlp/utils/_utils.py
diff options
context:
space:
mode:
Diffstat (limited to 'yt_dlp/utils/_utils.py')
-rw-r--r--yt_dlp/utils/_utils.py114
1 files changed, 106 insertions, 8 deletions
diff --git a/yt_dlp/utils/_utils.py b/yt_dlp/utils/_utils.py
index 664675a09..b5e1e2950 100644
--- a/yt_dlp/utils/_utils.py
+++ b/yt_dlp/utils/_utils.py
@@ -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 @@ MEDIA_EXTENSIONS.audio += MEDIA_EXTENSIONS.common_audio
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(...):