@@ -1717,39 +1717,6 @@ def random_user_agent():
1717
1717
'PST' : - 8 , 'PDT' : - 7 # Pacific
1718
1718
}
1719
1719
1720
-
1721
- class Namespace (object ):
1722
- """Immutable namespace"""
1723
-
1724
- def __init__ (self , ** kw_attr ):
1725
- self .__dict__ .update (kw_attr )
1726
-
1727
- def __iter__ (self ):
1728
- return iter (self .__dict__ .values ())
1729
-
1730
- @property
1731
- def items_ (self ):
1732
- return self .__dict__ .items ()
1733
-
1734
-
1735
- MEDIA_EXTENSIONS = Namespace (
1736
- common_video = ('avi' , 'flv' , 'mkv' , 'mov' , 'mp4' , 'webm' ),
1737
- video = ('3g2' , '3gp' , 'f4v' , 'mk3d' , 'divx' , 'mpg' , 'ogv' , 'm4v' , 'wmv' ),
1738
- common_audio = ('aiff' , 'alac' , 'flac' , 'm4a' , 'mka' , 'mp3' , 'ogg' , 'opus' , 'wav' ),
1739
- audio = ('aac' , 'ape' , 'asf' , 'f4a' , 'f4b' , 'm4b' , 'm4p' , 'm4r' , 'oga' , 'ogx' , 'spx' , 'vorbis' , 'wma' , 'weba' ),
1740
- thumbnails = ('jpg' , 'png' , 'webp' ),
1741
- # storyboards=('mhtml', ),
1742
- subtitles = ('srt' , 'vtt' , 'ass' , 'lrc' , 'ttml' ),
1743
- manifests = ('f4f' , 'f4m' , 'm3u8' , 'smil' , 'mpd' ),
1744
- )
1745
- MEDIA_EXTENSIONS .video = MEDIA_EXTENSIONS .common_video + MEDIA_EXTENSIONS .video
1746
- MEDIA_EXTENSIONS .audio = MEDIA_EXTENSIONS .common_audio + MEDIA_EXTENSIONS .audio
1747
-
1748
- KNOWN_EXTENSIONS = (
1749
- MEDIA_EXTENSIONS .video + MEDIA_EXTENSIONS .audio
1750
- + MEDIA_EXTENSIONS .manifests
1751
- )
1752
-
1753
1720
# needed for sanitizing filenames in restricted mode
1754
1721
ACCENT_CHARS = dict (zip ('ÂÃÄÀÁÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖŐØŒÙÚÛÜŰÝÞßàáâãäåæçèéêëìíîïðñòóôõöőøœùúûüűýþÿ' ,
1755
1722
itertools .chain ('AAAAAA' , ['AE' ], 'CEEEEIIIIDNOOOOOOO' , ['OE' ], 'UUUUUY' , ['TH' , 'ss' ],
@@ -3977,19 +3944,22 @@ def parse_duration(s):
3977
3944
return duration
3978
3945
3979
3946
3980
- def prepend_extension ( filename , ext , expected_real_ext = None ):
3947
+ def _change_extension ( prepend , filename , ext , expected_real_ext = None ):
3981
3948
name , real_ext = os .path .splitext (filename )
3982
- return (
3983
- '{0}.{1}{2}' .format (name , ext , real_ext )
3984
- if not expected_real_ext or real_ext [1 :] == expected_real_ext
3985
- else '{0}.{1}' .format (filename , ext ))
3949
+ sanitize_extension = _UnsafeExtensionError .sanitize_extension
3986
3950
3951
+ if not expected_real_ext or real_ext .partition ('.' )[0 ::2 ] == ('' , expected_real_ext ):
3952
+ filename = name
3953
+ if prepend and real_ext :
3954
+ sanitize_extension (ext , prepend = prepend )
3955
+ return '' .join ((filename , '.' , ext , real_ext ))
3987
3956
3988
- def replace_extension (filename , ext , expected_real_ext = None ):
3989
- name , real_ext = os .path .splitext (filename )
3990
- return '{0}.{1}' .format (
3991
- name if not expected_real_ext or real_ext [1 :] == expected_real_ext else filename ,
3992
- ext )
3957
+ # Mitigate path traversal and file impersonation attacks
3958
+ return '.' .join ((filename , sanitize_extension (ext )))
3959
+
3960
+
3961
+ prepend_extension = functools .partial (_change_extension , True )
3962
+ replace_extension = functools .partial (_change_extension , False )
3993
3963
3994
3964
3995
3965
def check_executable (exe , args = []):
@@ -6579,3 +6549,136 @@ def join_nonempty(*values, **kwargs):
6579
6549
if from_dict is not None :
6580
6550
values = (traverse_obj (from_dict , variadic (v )) for v in values )
6581
6551
return delim .join (map (compat_str , filter (None , values )))
6552
+
6553
+
6554
+ class Namespace (object ):
6555
+ """Immutable namespace"""
6556
+
6557
+ def __init__ (self , ** kw_attr ):
6558
+ self .__dict__ .update (kw_attr )
6559
+
6560
+ def __iter__ (self ):
6561
+ return iter (self .__dict__ .values ())
6562
+
6563
+ @property
6564
+ def items_ (self ):
6565
+ return self .__dict__ .items ()
6566
+
6567
+
6568
+ MEDIA_EXTENSIONS = Namespace (
6569
+ common_video = ('avi' , 'flv' , 'mkv' , 'mov' , 'mp4' , 'webm' ),
6570
+ video = ('3g2' , '3gp' , 'f4v' , 'mk3d' , 'divx' , 'mpg' , 'ogv' , 'm4v' , 'wmv' ),
6571
+ common_audio = ('aiff' , 'alac' , 'flac' , 'm4a' , 'mka' , 'mp3' , 'ogg' , 'opus' , 'wav' ),
6572
+ audio = ('aac' , 'ape' , 'asf' , 'f4a' , 'f4b' , 'm4b' , 'm4p' , 'm4r' , 'oga' , 'ogx' , 'spx' , 'vorbis' , 'wma' , 'weba' ),
6573
+ thumbnails = ('jpg' , 'png' , 'webp' ),
6574
+ # storyboards=('mhtml', ),
6575
+ subtitles = ('srt' , 'vtt' , 'ass' , 'lrc' , 'ttml' ),
6576
+ manifests = ('f4f' , 'f4m' , 'm3u8' , 'smil' , 'mpd' ),
6577
+ )
6578
+ MEDIA_EXTENSIONS .video = MEDIA_EXTENSIONS .common_video + MEDIA_EXTENSIONS .video
6579
+ MEDIA_EXTENSIONS .audio = MEDIA_EXTENSIONS .common_audio + MEDIA_EXTENSIONS .audio
6580
+
6581
+ KNOWN_EXTENSIONS = (
6582
+ MEDIA_EXTENSIONS .video + MEDIA_EXTENSIONS .audio
6583
+ + MEDIA_EXTENSIONS .manifests
6584
+ )
6585
+
6586
+
6587
+ class _UnsafeExtensionError (Exception ):
6588
+ """
6589
+ Mitigation exception for unwanted file overwrite/path traversal
6590
+ This should be caught in YoutubeDL.py with a warning
6591
+
6592
+ Ref: https://github.com/yt-dlp/yt-dlp/security/advisories/GHSA-79w7-vh3h-8g4j
6593
+ """
6594
+ _ALLOWED_EXTENSIONS = frozenset (itertools .chain (
6595
+ ( # internal
6596
+ 'description' ,
6597
+ 'json' ,
6598
+ 'meta' ,
6599
+ 'orig' ,
6600
+ 'part' ,
6601
+ 'temp' ,
6602
+ 'uncut' ,
6603
+ 'unknown_video' ,
6604
+ 'ytdl' ,
6605
+ ),
6606
+ # video
6607
+ MEDIA_EXTENSIONS .video , (
6608
+ 'avif' ,
6609
+ 'ismv' ,
6610
+ 'm2ts' ,
6611
+ 'm4s' ,
6612
+ 'mng' ,
6613
+ 'mpeg' ,
6614
+ 'qt' ,
6615
+ 'swf' ,
6616
+ 'ts' ,
6617
+ 'vp9' ,
6618
+ 'wvm' ,
6619
+ ),
6620
+ # audio
6621
+ MEDIA_EXTENSIONS .audio , (
6622
+ 'isma' ,
6623
+ 'mid' ,
6624
+ 'mpga' ,
6625
+ 'ra' ,
6626
+ ),
6627
+ # image
6628
+ MEDIA_EXTENSIONS .thumbnails , (
6629
+ 'bmp' ,
6630
+ 'gif' ,
6631
+ 'ico' ,
6632
+ 'heic' ,
6633
+ 'jng' ,
6634
+ 'jpeg' ,
6635
+ 'jxl' ,
6636
+ 'svg' ,
6637
+ 'tif' ,
6638
+ 'wbmp' ,
6639
+ ),
6640
+ # subtitle
6641
+ MEDIA_EXTENSIONS .subtitles , (
6642
+ 'dfxp' ,
6643
+ 'fs' ,
6644
+ 'ismt' ,
6645
+ 'sami' ,
6646
+ 'scc' ,
6647
+ 'ssa' ,
6648
+ 'tt' ,
6649
+ ),
6650
+ # others
6651
+ MEDIA_EXTENSIONS .manifests ,
6652
+ (
6653
+ # not used in yt-dl
6654
+ # *MEDIA_EXTENSIONS.storyboards,
6655
+ # 'desktop',
6656
+ # 'ism',
6657
+ # 'm3u',
6658
+ # 'sbv',
6659
+ # 'swp',
6660
+ # 'url',
6661
+ # 'webloc',
6662
+ # 'xml',
6663
+ )))
6664
+
6665
+ def __init__ (self , extension ):
6666
+ super (_UnsafeExtensionError , self ).__init__ ('unsafe file extension: {0!r}' .format (extension ))
6667
+ self .extension = extension
6668
+
6669
+ @classmethod
6670
+ def sanitize_extension (cls , extension , ** kwargs ):
6671
+ # ... /, *, prepend=False
6672
+ prepend = kwargs .get ('prepend' , False )
6673
+
6674
+ if '/' in extension or '\\ ' in extension :
6675
+ raise cls (extension )
6676
+
6677
+ if not prepend :
6678
+ last = extension .rpartition ('.' )[- 1 ]
6679
+ if last == 'bin' :
6680
+ extension = last = 'unknown_video'
6681
+ if last .lower () not in cls ._ALLOWED_EXTENSIONS :
6682
+ raise cls (extension )
6683
+
6684
+ return extension
0 commit comments