From b6590aaa1e3808155d69c9a79a797ae484163789 Mon Sep 17 00:00:00 2001 From: bashonly Date: Thu, 11 Jun 2026 00:38:20 +0000 Subject: [PATCH] Validate and escape values in `--write-link` output See https://github.com/yt-dlp/yt-dlp/security/advisories/GHSA-6v4j-43gg-vj32 Authored by: bashonly --- test/test_utils.py | 26 +++++++++++++++++++++++++- yt_dlp/YoutubeDL.py | 15 ++++++++++++--- yt_dlp/utils/_utils.py | 18 +++++++++++++++++- 3 files changed, 54 insertions(+), 5 deletions(-) diff --git a/test/test_utils.py b/test/test_utils.py index 429145ba6c..3aa248b96a 100644 --- a/test/test_utils.py +++ b/test/test_utils.py @@ -134,7 +134,10 @@ from yt_dlp.utils import ( xpath_text, xpath_with_ns, ) -from yt_dlp.utils._utils import _UnsafeExtensionError +from yt_dlp.utils._utils import ( + _UnsafeExtensionError, + _desktop_entry_localestring, +) from yt_dlp.utils.networking import ( HTTPHeaderDict, escape_rfc3986, @@ -1948,6 +1951,27 @@ Line 1 self.assertEqual( iri_to_uri('http://导航.中国/'), 'http://xn--fet810g.xn--fiqs8s/') + self.assertEqual( + iri_to_uri('file://example.org/run.exe', allowed_schemes=('file',)), + 'file://example.org/run.exe') + self.assertRaises(ValueError, iri_to_uri, 'file://example.org/run.exe') + + def test_desktop_entry_localestring(self): + self.assertEqual( + _desktop_entry_localestring('A B'), + 'A\\sB') + self.assertEqual( + _desktop_entry_localestring('A\nB'), + 'A\\nB') + self.assertEqual( + _desktop_entry_localestring('A\tB'), + 'A\\tB') + self.assertEqual( + _desktop_entry_localestring('A\rB'), + 'A\\rB') + self.assertEqual( + _desktop_entry_localestring('A\\B'), + 'A\\\\B') def test_clean_podcast_url(self): self.assertEqual(clean_podcast_url('https://www.podtrac.com/pts/redirect.mp3/chtbl.com/track/5899E/traffic.megaphone.fm/HSW7835899191.mp3'), 'https://traffic.megaphone.fm/HSW7835899191.mp3') diff --git a/yt_dlp/YoutubeDL.py b/yt_dlp/YoutubeDL.py index 70c14be8b3..6f244a4666 100644 --- a/yt_dlp/YoutubeDL.py +++ b/yt_dlp/YoutubeDL.py @@ -170,7 +170,12 @@ from .utils import ( write_json_file, write_string, ) -from .utils._utils import _UnsafeExtensionError, _YDLLogger, _ProgressState +from .utils._utils import ( + _UnsafeExtensionError, + _YDLLogger, + _ProgressState, + _desktop_entry_localestring, +) from .utils.networking import ( HTTPHeaderDict, clean_headers, @@ -3406,10 +3411,13 @@ class YoutubeDL: # Write internet shortcut files def _write_link_file(link_type): + # iri_to_uri converts to ascii, percent-escapes unsafe characters & validates scheme + # See https://github.com/yt-dlp/yt-dlp/security/advisories/GHSA-6v4j-43gg-vj32 url = try_get(info_dict['webpage_url'], iri_to_uri) if not url: self.report_warning( - f'Cannot write internet shortcut file because the actual URL of "{info_dict["webpage_url"]}" is unknown') + f'Cannot write internet shortcut file because the actual URL ' + f'of "{info_dict["webpage_url"]}" is unknown or disallowed') return True linkfn = replace_extension( self.prepare_filename(info_dict, 'link'), link_type, @@ -3425,7 +3433,8 @@ class YoutubeDL: newline='\r\n' if link_type == 'url' else '\n') as linkfile: template_vars = {'url': url} if link_type == 'desktop': - template_vars['filename'] = linkfn[:-(len(link_type) + 1)] + # See https://github.com/yt-dlp/yt-dlp/security/advisories/GHSA-6v4j-43gg-vj32 + template_vars['filename'] = _desktop_entry_localestring(linkfn[:-(len(link_type) + 1)]) linkfile.write(LINK_TEMPLATES[link_type] % template_vars) except OSError: self.report_error(f'Cannot write internet shortcut {linkfn}') diff --git a/yt_dlp/utils/_utils.py b/yt_dlp/utils/_utils.py index 856a565053..a791556eb5 100644 --- a/yt_dlp/utils/_utils.py +++ b/yt_dlp/utils/_utils.py @@ -4631,8 +4631,21 @@ LINK_TEMPLATES = { 'webloc': DOT_WEBLOC_LINK_TEMPLATE, } +# Ref: https://specifications.freedesktop.org/desktop-entry/latest/value-types.html +_DESKTOP_ENTRY_TRANS = str.maketrans({ + ' ': R'\s', + '\n': R'\n', + '\t': R'\t', + '\r': R'\r', + '\\': R'\\', +}) -def iri_to_uri(iri): + +def _desktop_entry_localestring(s): + return s.translate(_DESKTOP_ENTRY_TRANS) + + +def iri_to_uri(iri, *, allowed_schemes=('http', 'https')): """ Converts an IRI (Internationalized Resource Identifier, allowing Unicode characters) to a URI (Uniform Resource Identifier, ASCII-only). @@ -4641,6 +4654,9 @@ def iri_to_uri(iri): iri_parts = urllib.parse.urlparse(iri) + if iri_parts.scheme not in allowed_schemes: + raise ValueError(f'"{iri_parts.scheme}" is not in allowed_schemes: {", ".join(allowed_schemes)}') + if '[' in iri_parts.netloc: raise ValueError('IPv6 URIs are not, yet, supported.') # Querying `.netloc`, when there's only one bracket, also raises a ValueError.