CVE-2026-53951
Copier: trust-prefix bypass via path traversal runs tasks unprompted
Copier is a library and CLI app for rendering project templates. In versions 9.5.0 through 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 .. is therefore granted trust yet resolves to a different, attacker-controlled template, whose tasks / migrations / jinja_extensions then run without the --trust prompt — arbitrary command execution. Version 9.15.2 patches the issue.
Attack profile
How easy it is to exploit
- Attack vector
- Local
- Complexity
- Low
- Privileges needed
- None
- User interaction
- Passive
What an attacker can affect
- Confidentiality
- High
- Integrity
- High
- Availability
- High
Exploit likelihood
What each source reports
CVE List Score used
8.8
Copier: trust-prefix bypass via path traversal runs tasks unprompted
Copier is a library and CLI app for rendering project templates. In versions 9.5.0 through 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 .. is therefore granted trust yet resolves to a different, attacker-controlled template, whose tasks / migrations / jinja_extensions then run without the --trust prompt — arbitrary command execution. Version 9.15.2 patches the issue.
- Record
CVE-2026-53951- CVSS 4.0
- 8.8 High
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 - Weakness
- CWE-22, CWE-94
- Updated
- 2026-11-02
EUVDEUVD-2026-42309
8.8
Copier is a library and CLI app for rendering project templates. In versions 9.5.0 through 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 .. is therefore granted trust yet resolves to a different, attacker-controlled template, whose tasks / migrations / jinja_extensions then run without the --trust prompt — arbitrary command execution. Version 9.15.2 patches the issue.
- Record
EUVD-2026-42309- CVSS 4.0
- 8.8 High
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 - Updated
- 2026-11-02
GitHub AdvisoriesGHSA-9gmc-jqmh-3rvm
8.8
Copier has a trust-prefix bypass via path traversal that runs tasks unprompted
Copier: trust-prefix bypass via path traversal runs tasks unprompted
Summary
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, whose
tasks / 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() plus
is_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 full
poc.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
…
- Record
GHSA-9gmc-jqmh-3rvm- CVSS 4.0
- 8.8 High
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 - Weakness
- CWE-22, CWE-94
- Updated
- 2026-11-02
NVD
8.8
Copier is a library and CLI app for rendering project templates. In versions 9.5.0 through 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 .. is therefore granted trust yet resolves to a different, attacker-controlled template, whose tasks / migrations / jinja_extensions then run without the --trust prompt — arbitrary command execution. Version 9.15.2 patches the issue.
- Record
CVE-2026-53951- CVSS 4.0
- 8.8 High
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/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X - Weakness
- CWE-22, CWE-94
- Updated
- 2026-11-02
OSV
8.8
Copier: trust-prefix bypass via path traversal runs tasks unprompted
Copier is a library and CLI app for rendering project templates. In versions 9.5.0 through 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 .. is therefore granted trust yet resolves to a different, attacker-controlled template, whose tasks / migrations / jinja_extensions then run without the --trust prompt — arbitrary command execution. Version 9.15.2 patches the issue.
- Record
CVE-2026-53951- CVSS 4.0
- 8.8 High
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 - Weakness
- CWE-22, CWE-94
- Updated
- 2026-11-02
OSVGHSA-9gmc-jqmh-3rvm
8.8
Copier has a trust-prefix bypass via path traversal that runs tasks unprompted
Copier: trust-prefix bypass via path traversal runs tasks unprompted
Summary
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, whose
tasks / 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() plus
is_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 full
poc.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
…
- Record
GHSA-9gmc-jqmh-3rvm- CVSS 4.0
- 8.8 High
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 - Weakness
- CWE-22, CWE-94
- Updated
- 2026-11-02
OSVPYSEC-2026-3818
8.8
Copier has a trust-prefix bypass via path traversal that runs tasks unprompted
Copier: trust-prefix bypass via path traversal runs tasks unprompted
Summary
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, whose
tasks / 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() plus
is_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 full
poc.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
…
- Record
PYSEC-2026-3818- CVSS 4.0
- 8.8 High
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 - Updated
- 2026-11-02
Affected products
Affected packages
History
| Recorded | Changed | Sources |
|---|---|---|
| first indexed | CVE List, EUVD, GitHub Advisories, NVD, OSV |
Dates are when Every CVE recorded each change. Upstream dates may differ.
In short
CVE-2026-53951 is a high vulnerability (CVSS 8.8) in Copier-org Copier, published July 8, 2026. No source lists public exploit code. EPSS puts the chance of exploitation in the next 30 days at 0.26%, the 17th percentile. Fixed in copier 9.15.2.
References
github.com/copier-org/copier/security/advisories/GHSA-9gmc-jqmh-3rvm
github.com/copier-org/copier/releases/tag/v9.15.2
nvd.nist.gov/vuln/detail/CVE-2026-53951
github.com/advisories/GHSA-9gmc-jqmh-3rvm
github.com/CVEProject/cvelistV5/tree/main/cves/2026/53xxx/CVE-2026-53951.json
github.com/copier-org/copier
pypi.org/project/copier
Left out here: the rest of 3 source descriptions. The JSON record has them.