Summary
Copier has a trust-prefix bypass via path traversal that runs tasks unprompted
Copier: trust-prefix bypass via path traversal runs tasks unprompted
In copier >= 9.5.0, <= 9.15.1, the trust setting's prefix match
(copier/_settings.py) compares the template URL against a trusted prefix with
a raw str.startswith and no path normalization, while the URL is
normalized when the template is actually fetched (Path.resolve() for local
paths; libcurl dot-segment removal for https). A template reference that
textually starts with a trusted prefix but contains ..
(e.g. https://github.com/trusted-org/../attacker-org/repo.git) is therefore
granted trust yet resolves to a different, attacker-controlled template, whosetasks / migrations / jinja_extensions then run without the --trust
prompt, arbitrary command execution. Likely CWE-22 (Improper Limitation of
a Pathname) in the trust check leading to CWE-94 (code execution).
Details
trust lets users mark template locations as trusted so copier skips the
unsafe-feature gate. A trailing / makes an entry a prefix match
(docs/settings.md: "Locations ending with / will be matched as prefixes,
trusting all templates from that location").
copier/_settings.py:141-146 (tag v9.15.1):
return any(
repository.startswith(_normalize(t))
if t.endswith("/")
else repository == _normalize(t)
for t in trust
)
_normalize only expands ~; it does not touch .. or collapse segments ,copier/_settings.py:149-152 (tag v9.15.1):
def _normalize(url: str) -> str:
if url.startswith("~"): # Only expand on str to avoid messing with URLs
url = expanduser(url) # noqa: PTH111
return url
This decision gates code execution, copier/_main.py:293 (tag v9.15.1):
if self.unsafe or is_trusted_repository(self.settings.trust, self.template.url):
return # skip the unsafe-feature check entirely
The chain: the trust comparison sees the raw URL, so"https://github.com/safeorg/../evilorg/t.git".startswith("https://github.com/safeorg/")
is True; but the value copier hands to git/pathlib is normalized, so
the template actually loaded is evilorg/t (a different, attacker-owned org).
Trust is granted to a location the user never trusted, and _check_unsafe
returns early, so the malicious template's tasks execute with no prompt.
This is most acute on copier update, which reads _src_path from the
project's .copier-answers.yml (copier/_subproject.py), i.e. an attacker who
hands you a project controls the URL that the trust check is applied to.
In-repo asymmetry that confirms the omission: copier consistently resolves
paths everywhere else it makes a security decision, Path.resolve() plusis_relative_to(...) guards in _render_template, template_copy_root, and_external_data, but not in the trust comparison.
PoC
Self-contained standalone script; runs against a clean, pinned PyPI install via
the real copier CLI only. Static by default (copier copy --pretend
reaches the trust decision but does not execute tasks); --prove-exec is an
opt-in supplementary run that fires an inert marker (echo + touch). The fullpoc.py accompanies this report.
Build and run:
python -m venv venv && . venv/bin/activate
pip install "copier==9.15.1"
python poc.py # static proof (default)
python poc.py --prove-exec # also fire the inert marker
Observed output (copier 9.15.1):
== version proof ==
copier == 9.15.1
module : .../site-packages/copier/__init__.py
== inputs ==
trusted prefix (settings.yml): /tmp/copier_trust_poc_XXXX/trusted_templates/
control src (canonical) : /tmp/copier_trust_poc_XXXX/attacker/evil_template
exploit src (traversal) : /tmp/copier_trust_poc_XXXX/trusted_templates/../attacker/evil_template
both resolve to the SAME dir : True
exploit startswith trusted/ : True
minimal delta : exploit = '/tmp/copier_trust_poc_XXXX/trusted_templates/..' + '/attacker/evil_template'
== static proof (copier copy --pretend; payload NOT executed) ==
control (canonical, untrusted): exit=4 -> BLOCKED (UnsafeTemplateError)
exploit (trusted-prefix /..) : exit=0 -> TRUSTED, task reached
marker on disk after --pretend: False (expected False: --pretend does not run tasks)
--- copier's own output for the exploit (note the task it WOULD run) ---
| Copying from template version None
| create hello.txt
| > Running task 1 of 1: echo COPIER-TRUST-BYPASS-RCE-MARKER && touch COPIER_RCE_PROOF
== VERDICT ==
BYPASS CONFIRMED: identical template is refused by canonical path
(exit 4) yet granted trust via '<trusted>/..' traversal (exit 0),
so its tasks run with no --trust prompt.
== --prove-exec: running the exploit for real (inert marker) ==
| COPIER-TRUST-BYPASS-RCE-MARKER
| > Running task 1 of 1: echo COPIER-TRUST-BYPASS-RCE-MARKER && touch COPIER_RCE_PROOF
exit=0 marker file 'COPIER_RCE_PROOF' created: True
-> ARBITRARY COMMAND EXECUTED via a 'trusted' template, no --trust
The exploit is the same template as the control plus the minimal delta<trusted_prefix>/... Deterministic: same input → same result. The PoC uses a
local trusted prefix for a self-contained, network-free run; the https case is
identical because git normalizes .. before the request, e.g.git ls-remote "https://github.com/copier-org/../pallets/flask.git" emitswarning: redirecting to https://github.com/pallets/flask.git/ and returnspallets/flask's refs, a different org than the trusted copier-org/.
References
- CWE-22, https://cwe.mitre.org/data/definitions/22.html
- CWE-94, https://cwe.mitre.org/data/definitions/94.html
- Affected source (tag
v9.15.1):copier/_settings.py:141-146(prefix match),copier/_settings.py:149-152(_normalize),copier/_main.py:293(trust gate). - Documented prefix behavior:
docs/settings.md("Locations ending with/
will be matched as prefixes"). https..normalization: libcurl removes dot segments by default
(CURLOPT_PATH_AS_ISdefaults to off), https://curl.se/libcurl/c/CURLOPT_PATH_AS_IS.html- Novelty: distinct from copier's published advisories, which concern filesystem
read/write traversal in rendered output; this is an authorization bypass in
thetrustsetting's URL matching. The flawed match is identical between
releasedv9.15.1and currentmasterHEAD, and unchanged since the
trust-prefix feature was introduced inv9.5.0(originallycopier/settings.py,
commit71358ed; renamed tocopier/_settings.pyin the v9.12.0 refactor).
Impact
A user who has configured a trusted prefix (a trailing-/ entry intrust, a documented feature) no longer gets the unsafe-feature prompt for a
template that merely appears to live under that prefix. Any party who can
influence the template URL, most realistically the author of a project the
victim runs copier update on, since _src_path comes from that project's.copier-answers.yml, can host the real template under a different
org/location reached via .. and have its tasks/migrations/jinja_extensions execute arbitrary commands with no prompt. It fires on a
default, modern git for both local paths and https.
Proposed severity: High, comparable to the project's prior unsafe-template
advisory (GHSA-3xw7-v6cj-5q8h). Proposed CVSS v4 vector (maintainer to finalize;AT:P reflects the required trusted-prefix configuration):CVSS:4.0/AV:L/AC:L/AT:P/PR:N/UI:P/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H. Conservative
variant if you scope impact to the user account only (no host escape claim):
drop SC/SI/SA to N.
Input manipulates file paths to reach files outside the intended directory, such as configuration or credential files. Typical impact: unauthorized file read or write outside the intended directory.
Affected versions
Security releases
Kodem intelligence
Severity tells you how bad this could be in the worst case. It does not tell you whether you are exposed. Exploitability and impact are functions of runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A vulnerable package can sit in your dependency tree and never run.
Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter. Kodem's runtime-powered SCA identifies whether this CVE is reachable in your applications.
Already deployed Kodem?
See it in your environmentNew to Kodem? Get a demo →Remediation advice
Normalize both sides before comparing, instead of raw startswith. For
local entries, compare resolved absolute paths (Path(t).resolve() vsPath(repository).resolve()) using segment containment / is_relative_to, the
pattern already used in _render_template and template_copy_root. For URL
entries, parse the URL and reject or collapse .././empty path segments
before the prefix test. As defense-in-depth, reject any _src_path read from an
answers file that contains .. segments after the scheme/host, since
legitimate template URLs never need them.
Frequently Asked Questions
- What is CVE-2026-53951? CVE-2026-53951 is a high-severity path traversal vulnerability in copier (pip), affecting versions >= 9.5.0, <= 9.15.1. It is fixed in 9.15.2. Input manipulates file paths to reach files outside the intended directory, such as configuration or credential files.
- Which versions of copier are affected by CVE-2026-53951? copier (pip) versions >= 9.5.0, <= 9.15.1 is affected.
- Is there a fix for CVE-2026-53951? Yes. CVE-2026-53951 is fixed in 9.15.2. Upgrade to this version or later.
- Is CVE-2026-53951 exploitable, and should I be worried? Whether CVE-2026-53951 is exploitable in your environment depends on whether the vulnerable code is present and reachable. A CVSS score is a worst-case rating; it does not account for your specific deployment, configuration, or usage patterns. Kodem, an Intelligent Application Security platform, uses runtime intelligence to show which vulnerabilities actually execute in production, so you can focus on the ones that represent real risk. Get a demo
- What actually determines whether CVE-2026-53951 is exploitable, and how bad it is? Exploitability and impact are not fixed properties of a CVE. They depend on runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A high CVSS score on a dependency that never runs is not the same as real risk. Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter.
- How do I fix CVE-2026-53951? Upgrade
copierto 9.15.2 or later.