yt-dlp command injection when using `%q` in `--exec` on Windows
Description
yt-dlp on Windows is vulnerable to remote code execution via shell command injection in the --exec flag's output template expansion with the %q conversion.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
yt-dlp on Windows is vulnerable to remote code execution via shell command injection in the --exec flag's output template expansion with the %q conversion.
Overview
CVE-2023-40581 is a command injection vulnerability in yt-dlp on Windows that allows remote code execution. The flaw exists in the --exec flag's output template expansion, specifically when the %q conversion is used to quote/escape metadata values. For the cmd shell used by Python's subprocess on Windows, the escaping does not properly handle special characters, enabling an attacker to inject arbitrary shell commands through maliciously crafted remote data [1][2].
Exploitation
An attacker can exploit this vulnerability by providing a video or metadata field that contains special characters such as double quotes, pipes, or ampersands, which bypass the insufficient escaping. If a user runs yt-dlp with --exec containing %q expansion on such content, the injected commands execute in the context of the user's shell. The attack requires no authentication beyond normal user access and works regardless of whether yt-dlp is invoked from cmd or PowerShell [2].
Impact
Successful exploitation grants the attacker arbitrary code execution on the Windows system. This can lead to data exfiltration, installation of malware, or full compromise of the affected machine. The vulnerability was introduced in yt-dlp version 2021.04.11 and affects all Windows users who use --exec with output template expansion [2].
Mitigation
The issue is fixed in yt-dlp version 2023.09.24, which replaces the shell escape function to use "" instead of \" and properly quotes commands [3][4]. Users are urged to update immediately. For those unable to upgrade, the advisory recommends avoiding output template expansion other than {} (filepath), verifying that fields used in --exec do not contain dangerous characters, or using JSON output and loading fields instead of using --exec [2].
AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
yt-dlpPyPI | >= 2021.04.11, < 2023.09.24 | 2023.09.24 |
Affected products
2- yt-dlp/yt-dlpv5Range: >= 2021.04.11, < 2023.09.24
Patches
1de015e930747[core] Prevent RCE when using `--exec` with `%q` (CVE-2023-40581)
6 files changed · +46 −13
devscripts/changelog_override.json+5 −0 modified@@ -93,5 +93,10 @@ "action": "add", "when": "c1d71d0d9f41db5e4306c86af232f5f6220a130b", "short": "[priority] **The minimum *recommended* Python version has been raised to 3.8**\nSince Python 3.7 has reached end-of-life, support for it will be dropped soon. [Read more](https://github.com/yt-dlp/yt-dlp/issues/7803)" + }, + { + "action": "add", + "when": "61bdf15fc7400601c3da1aa7a43917310a5bf391", + "short": "[priority] Security: [[CVE-2023-40581](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-40581)] [Prevent RCE when using `--exec` with `%q` on Windows](https://github.com/yt-dlp/yt-dlp/security/advisories/GHSA-42h4-v29r-42qg)\n - The shell escape function is now using `\"\"` instead of `\\\"`.\n - `utils.Popen` has been patched to properly quote commands." } ]
test/test_utils.py+16 −0 modified@@ -14,6 +14,7 @@ import io import itertools import json +import subprocess import xml.etree.ElementTree from yt_dlp.compat import ( @@ -28,6 +29,7 @@ InAdvancePagedList, LazyList, OnDemandPagedList, + Popen, age_restricted, args_to_str, base_url, @@ -2388,6 +2390,20 @@ def test_extract_basic_auth(self): assert extract_basic_auth('http://user:@foo.bar') == ('http://foo.bar', 'Basic dXNlcjo=') assert extract_basic_auth('http://user:pass@foo.bar') == ('http://foo.bar', 'Basic dXNlcjpwYXNz') + @unittest.skipUnless(compat_os_name == 'nt', 'Only relevant on Windows') + def test_Popen_windows_escaping(self): + def run_shell(args): + stdout, stderr, error = Popen.run( + args, text=True, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + assert not stderr + assert not error + return stdout + + # Test escaping + assert run_shell(['echo', 'test"&']) == '"test""&"\n' + # Test if delayed expansion is disabled + assert run_shell(['echo', '^!']) == '"^!"\n' + assert run_shell('echo "^!"') == '"^!"\n' if __name__ == '__main__': unittest.main()
test/test_YoutubeDL.py+3 −3 modified@@ -784,9 +784,9 @@ def expect_same_infodict(out): test('%(title4)#S', 'foo_bar_test') test('%(title4).10S', ('foo "bar" ', 'foo "bar"' + ('#' if compat_os_name == 'nt' else ' '))) if compat_os_name == 'nt': - test('%(title4)q', ('"foo \\"bar\\" test"', ""foo ⧹"bar⧹" test"")) - test('%(formats.:.id)#q', ('"id 1" "id 2" "id 3"', '"id 1" "id 2" "id 3"')) - test('%(formats.0.id)#q', ('"id 1"', '"id 1"')) + test('%(title4)q', ('"foo ""bar"" test"', None)) + test('%(formats.:.id)#q', ('"id 1" "id 2" "id 3"', None)) + test('%(formats.0.id)#q', ('"id 1"', None)) else: test('%(title4)q', ('\'foo "bar" test\'', '\'foo "bar" test\'')) test('%(formats.:.id)#q', "'id 1' 'id 2' 'id 3'")
yt_dlp/compat/__init__.py+1 −1 modified@@ -30,7 +30,7 @@ def compat_etree_fromstring(text): if compat_os_name == 'nt': def compat_shlex_quote(s): import re - return s if re.match(r'^[-_\w./]+$', s) else '"%s"' % s.replace('"', '\\"') + return s if re.match(r'^[-_\w./]+$', s) else s.replace('"', '""').join('""') else: from shlex import quote as compat_shlex_quote # noqa: F401
yt_dlp/postprocessor/exec.py+5 −7 modified@@ -1,8 +1,6 @@ -import subprocess - from .common import PostProcessor from ..compat import compat_shlex_quote -from ..utils import PostProcessingError, encodeArgument, variadic +from ..utils import Popen, PostProcessingError, variadic class ExecPP(PostProcessor): @@ -27,10 +25,10 @@ def parse_cmd(self, cmd, info): def run(self, info): for tmpl in self.exec_cmd: cmd = self.parse_cmd(tmpl, info) - self.to_screen('Executing command: %s' % cmd) - retCode = subprocess.call(encodeArgument(cmd), shell=True) - if retCode != 0: - raise PostProcessingError('Command returned error code %d' % retCode) + self.to_screen(f'Executing command: {cmd}') + _, _, return_code = Popen.run(cmd, shell=True) + if return_code != 0: + raise PostProcessingError(f'Command returned error code {return_code}') return [], info
yt_dlp/utils/_utils.py+16 −2 modified@@ -825,7 +825,7 @@ def _fix(key): _fix('LD_LIBRARY_PATH') # Linux _fix('DYLD_LIBRARY_PATH') # macOS - def __init__(self, *args, env=None, text=False, **kwargs): + def __init__(self, args, *remaining, env=None, text=False, shell=False, **kwargs): if env is None: env = os.environ.copy() self._fix_pyinstaller_ld_path(env) @@ -835,7 +835,21 @@ def __init__(self, *args, env=None, text=False, **kwargs): kwargs['universal_newlines'] = True # For 3.6 compatibility kwargs.setdefault('encoding', 'utf-8') kwargs.setdefault('errors', 'replace') - super().__init__(*args, env=env, **kwargs, startupinfo=self._startupinfo) + + if shell and compat_os_name == 'nt' and kwargs.get('executable') is None: + if not isinstance(args, str): + args = ' '.join(compat_shlex_quote(a) for a in args) + shell = False + args = f'{self.__comspec()} /Q /S /D /V:OFF /C "{args}"' + + super().__init__(args, *remaining, env=env, shell=shell, **kwargs, startupinfo=self._startupinfo) + + def __comspec(self): + comspec = os.environ.get('ComSpec') or os.path.join( + os.environ.get('SystemRoot', ''), 'System32', 'cmd.exe') + if os.path.isabs(comspec): + return comspec + raise FileNotFoundError('shell not found: neither %ComSpec% nor %SystemRoot% is set') def communicate_or_kill(self, *args, **kwargs): try:
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
7- github.com/advisories/GHSA-42h4-v29r-42qgghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2023-40581ghsaADVISORY
- github.com/yt-dlp/yt-dlp-nightly-builds/releases/tag/2023.09.24.003044ghsax_refsource_MISCWEB
- github.com/yt-dlp/yt-dlp/commit/de015e930747165dbb8fcd360f8775fd973b7d6eghsax_refsource_MISCWEB
- github.com/yt-dlp/yt-dlp/releases/tag/2021.04.11ghsax_refsource_MISCWEB
- github.com/yt-dlp/yt-dlp/releases/tag/2023.09.24ghsax_refsource_MISCWEB
- github.com/yt-dlp/yt-dlp/security/advisories/GHSA-42h4-v29r-42qgghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.