GHSA-9gmc-jqmh-3rvm: Path Traversal
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 / jinjaextensions 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):
python 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):
python 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):
python if self.unsafe or istrustedrepository(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 checkunsafe returns early, so the malicious template's tasks execute with no prompt.
This is most acute on copier update, which reads srcpath 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 isrelativeto(...) guards in rendertemplate, templatecopyroot, and externaldata — 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:
bash 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/copiertrustpocXXXX/trustedtemplates/ control src (canonical) : /tmp/copiertrustpocXXXX/attacker/eviltemplate exploit src (traversal) : /tmp/copiertrustpocXXXX/trustedtemplates/../attacker/eviltemplate both resolve to the SAME dir : True exploit startswith trusted/ : True minimal delta : exploit = '/tmp/copiertrustpocXXXX/trustedtemplates/..' + '/attacker/eviltemplate'
== 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 COPIERRCEPROOF
== 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 COPIERRCEPROOF exit=0 marker file 'COPIERRCEPROOF' created: True -> ARBITRARY COMMAND EXECUTED via a 'trusted' template, no --trust
The exploit is the same template as the control plus the minimal delta <trustedprefix>/... 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" emits warning: redirecting to https://github.com/pallets/flask.git/ and returns pallets/flask's refs, a different org than the trusted copier-org/.
Impact
A user who has configured a trusted prefix (a trailing-/ entry in trust, 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 srcpath 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/ jinjaextensions 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.
Recommended fix
Normalize both sides before comparing, instead of raw startswith. For local entries, compare resolved absolute paths (Path(t).resolve() vs Path(repository).resolve()) using segment containment / isrelativeto, the pattern already used in rendertemplate and templatecopyroot. For URL entries, parse the URL and reject or collapse .././empty path segments before the prefix test. As defense-in-depth, reject any srcpath read from an answers file that contains .. segments after the scheme/host, since legitimate template URLs never need them.
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 (CURLOPTPATHASIS defaults to off) — https://curl.se/libcurl/c/CURLOPTPATHASIS.html - Novelty: distinct from copier's published advisories, which concern filesystem read/write traversal in rendered output; this is an authorization bypass in the trust setting's URL matching. The flawed match is identical between released v9.15.1 and current master HEAD, and unchanged since the trust-prefix feature was introduced in v9.5.0 (originally copier/settings.py, commit 71358ed; renamed to copier/settings.py in the v9.12.0 refactor).
Affected Software
Remediation
Recommended actions to resolve this vulnerability, in priority order.
- Upgrade
Upgrade
pip/copierto a version that resolves this vulnerability.Fixed in 9.15.2
Event History
Frequently Asked Questions
Who is exposed to this issue?
Users of Copier versions 9.5.0 through 9.15.1 are exposed if their trust settings contain a trailing-slash location that is used as a trusted prefix. The issue affects both local-path and HTTPS template references because those references are normalized when fetched but not when checked against the trust prefix.
What does an attacker need to exploit it?
An attacker needs to cause a user to use a template reference that textually begins with a configured trusted prefix but includes a .. path segment that resolves to an attacker-controlled location. The attacker-controlled template must contain unsafe features such as tasks, migrations, or jinja_extensions, which then execute without the usual --trust prompt.
Are default Copier configurations affected?
The provided information identifies the vulnerable condition as use of trust entries ending in /, which enable prefix matching. It does not establish that such trusted-prefix entries are present by default.
What should be done if updating is not immediately possible?
Avoid trusting broad prefix locations ending in / and do not use template references containing .. segments. Review template URLs before running Copier and require explicit trust confirmation for templates rather than relying on prefix-based trust.
How can I determine whether I may already be affected?
Check whether Copier is version 9.5.0 through 9.15.1 and inspect its trust configuration for trailing-slash trusted locations. Review template references used in automation or user workflows for .. segments that begin with a trusted prefix but resolve outside that location.