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
This commit is contained in:
bashonly 2026-06-11 00:38:20 +00:00
parent bed3d58c5c
commit b6590aaa1e
No known key found for this signature in database
GPG Key ID: 783F096F253D15B0
3 changed files with 54 additions and 5 deletions

View File

@ -134,7 +134,10 @@ from yt_dlp.utils import (
xpath_text, xpath_text,
xpath_with_ns, 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 ( from yt_dlp.utils.networking import (
HTTPHeaderDict, HTTPHeaderDict,
escape_rfc3986, escape_rfc3986,
@ -1948,6 +1951,27 @@ Line 1
self.assertEqual( self.assertEqual(
iri_to_uri('http://导航.中国/'), iri_to_uri('http://导航.中国/'),
'http://xn--fet810g.xn--fiqs8s/') '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): 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') 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')

View File

@ -170,7 +170,12 @@ from .utils import (
write_json_file, write_json_file,
write_string, write_string,
) )
from .utils._utils import _UnsafeExtensionError, _YDLLogger, _ProgressState from .utils._utils import (
_UnsafeExtensionError,
_YDLLogger,
_ProgressState,
_desktop_entry_localestring,
)
from .utils.networking import ( from .utils.networking import (
HTTPHeaderDict, HTTPHeaderDict,
clean_headers, clean_headers,
@ -3406,10 +3411,13 @@ class YoutubeDL:
# Write internet shortcut files # Write internet shortcut files
def _write_link_file(link_type): 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) url = try_get(info_dict['webpage_url'], iri_to_uri)
if not url: if not url:
self.report_warning( 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 return True
linkfn = replace_extension( linkfn = replace_extension(
self.prepare_filename(info_dict, 'link'), link_type, 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: newline='\r\n' if link_type == 'url' else '\n') as linkfile:
template_vars = {'url': url} template_vars = {'url': url}
if link_type == 'desktop': 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) linkfile.write(LINK_TEMPLATES[link_type] % template_vars)
except OSError: except OSError:
self.report_error(f'Cannot write internet shortcut {linkfn}') self.report_error(f'Cannot write internet shortcut {linkfn}')

View File

@ -4631,8 +4631,21 @@ LINK_TEMPLATES = {
'webloc': DOT_WEBLOC_LINK_TEMPLATE, '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). 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) 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: if '[' in iri_parts.netloc:
raise ValueError('IPv6 URIs are not, yet, supported.') raise ValueError('IPv6 URIs are not, yet, supported.')
# Querying `.netloc`, when there's only one bracket, also raises a ValueError. # Querying `.netloc`, when there's only one bracket, also raises a ValueError.