Vulnerabilities in pkg:pypi/copier

PACKAGE
Advisory Identifier: GHSA-9gmc-jqmh-3rvm
Advisory Summary

CVE-2026-53951 is a HIGH path traversal vulnerability (CWE-94: Improper Control of Generation of Code ('Code Injection')) affecting Copier libraries in the PyPI ecosystem. The issue involves copier has a trust-prefix bypass via path traversal that runs tasks unprompted. Remediation is available by upgrading affected packages to version 9.15.2.

7 Known Advisory Record(s)
Package Vulnerability Intelligence

pkg:pypi/copier

7 Known Advisory Record(s)

Vulnerability advisories affecting package pkg:pypi/copier. Select an advisory below to view technical details or fixes.

Advisory ID CVE / NVD Severity Summary
GHSA-9gmc-jqmh-3rvm CVE-2026-53951 HIGH Copier has a trust-prefix bypass via path traversal that runs tasks unprompted
GHSA-hgjq-p8cr-gg4h CVE-2026-34730 MODERATE Copier `_external_data` allows path traversal and absolute-path local file read without unsafe mode
GHSA-85v3-4m8g-hrh6 CVE-2026-34726 MODERATE Copier `_subdirectory` allows template root escape via parent-directory traversal
GHSA-4fqp-r85r-hxqh CVE-2026-23986 MODERATE Copier safe template has arbitrary filesystem write access via directory symlinks when _preserve_symlinks: true
GHSA-xjhm-gp88-8pfx CVE-2026-23968 MODERATE Copier safe template has arbitrary filesystem read access via symlinks when _preserve_symlinks: false
GHSA-p7q8-grrj-3m8w CVE-2025-55214 MODERATE Copier's safe template has filesystem write access outside destination path
GHSA-3xw7-v6cj-5q8h CVE-2025-55201 HIGH Copier's safe template has arbitrary filesystem read/write access

CVE-2026-53951 at a Glance

CVE Identifier CVE-2026-53951
GHSA Identifier GHSA-9gmc-jqmh-3rvm
Severity HIGH
CVSS v3.1 Base Score N/A
CWE Classification CWE-94: Improper Control of Generation of Code ('Code Injection')
Affected Product Copier
Package Ecosystem PyPI
Published Date
Last Updated

How to Fix CVE-2026-53951

To remediate this vulnerability, upgrade all affected Copier dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

copier fixed: 9.15.2
pip install copier>=9.15.2
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

pip show copier

How to Check if You Are Affected

To determine whether your Python environment is affected by CVE-2026-53951, inspect your installed packages using pip:

pip show copier

Analyze the terminal output to locate any version of Copier in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 9.15.2), your application is affected.

Note: The pip show copier command checks currently installed packages in your active Python environment.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-94: Improper Control of Generation of Code ('Code Injection'), where the product constructs all or part of a code segment using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the syntax or behavior of the intended code segment. In Copier, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

# 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`): ```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 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: ```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/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"` 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 `_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`. ### 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 / `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. ### 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_IS` defaults 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 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).

CVE-2026-53951 Affected Copier Versions

1 component(s)
Fixed Versions:
9.15.2
Product / Package Ecosystem Affected Versions Fixed Version
copier
PyPI
≥ 9.5.0
< 9.15.2
9.15.2

References

Frequently Asked Questions

What is CVE-2026-53951?
CVE-2026-53951 is a security vulnerability in Copier involving Copier has a trust-prefix bypass via path traversal that runs tasks unprompted under CWE-94: Improper Control of Generation of Code ('Code Injection').
Which Copier versions are affected?
Affected versions include releases prior to 9.15.2 across affected packages: copier.
What version fixes CVE-2026-53951?
Upgrading to 9.15.2 (or the latest patched release for your respective branch) resolves the vulnerability.
Is copier affected?
Yes, copier is affected in releases prior to 9.15.2. Upgrade your dependency to 9.15.2 or later.
What CWE is associated with CVE-2026-53951?
CVE-2026-53951 is classified under CWE-94: Improper Control of Generation of Code ('Code Injection') (The product constructs all or part of a code segment using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the syntax or behavior of the intended code segment.).
How can I check PyPI dependencies for CVE-2026-53951?
Run the dependency tree command: 'pip show copier'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-9gmc-jqmh-3rvm",
  "modified" : "2026-08-19T19:16:54Z",
  "published" : "2026-08-19T19:16:54Z",
  "aliases" : [ "CVE-2026-53951" ],
  "summary" : "Copier has a trust-prefix bypass via path traversal that runs tasks unprompted",
  "details" : "# Copier: trust-prefix bypass via path traversal runs tasks unprompted\n\n### Summary\n\nIn copier `>= 9.5.0, <= 9.15.1`, the `trust` setting's prefix match\n(`copier/_settings.py`) compares the template URL against a trusted prefix with\na raw `str.startswith` and **no path normalization**, while the URL *is*\nnormalized when the template is actually fetched (`Path.resolve()` for local\npaths; libcurl dot-segment removal for `https`). A template reference that\ntextually starts with a trusted prefix but contains `..`\n(e.g. `https://github.com/trusted-org/../attacker-org/repo.git`) is therefore\ngranted trust yet resolves to a different, attacker-controlled template, whose\n`tasks` / `migrations` / `jinja_extensions` then run **without the `--trust`\nprompt** — arbitrary command execution. Likely **CWE-22 (Improper Limitation of\na Pathname)** in the trust check leading to **CWE-94 (code execution)**.\n\n### Details\n\n`trust` lets users mark template locations as trusted so copier skips the\nunsafe-feature gate. A trailing `/` makes an entry a **prefix** match\n(`docs/settings.md`: *\"Locations ending with `/` will be matched as prefixes,\ntrusting all templates from that location\"*).\n\n`copier/_settings.py:141-146` (tag `v9.15.1`):\n\n```python\n    return any(\n        repository.startswith(_normalize(t))\n        if t.endswith(\"/\")\n        else repository == _normalize(t)\n        for t in trust\n    )\n```\n\n`_normalize` only expands `~`; it does **not** touch `..` or collapse segments —\n`copier/_settings.py:149-152` (tag `v9.15.1`):\n\n```python\ndef _normalize(url: str) -> str:\n    if url.startswith(\"~\"):  # Only expand on str to avoid messing with URLs\n        url = expanduser(url)  # noqa: PTH111\n    return url\n```\n\nThis decision gates code execution — `copier/_main.py:293` (tag `v9.15.1`):\n\n```python\n        if self.unsafe or is_trusted_repository(self.settings.trust, self.template.url):\n            return  # skip the unsafe-feature check entirely\n```\n\nThe chain: the trust comparison sees the **raw** URL, so\n`\"https://github.com/safeorg/../evilorg/t.git\".startswith(\"https://github.com/safeorg/\")`\nis `True`; but the value copier hands to `git`/`pathlib` is **normalized**, so\nthe template actually loaded is `evilorg/t` (a different, attacker-owned org).\nTrust is granted to a location the user never trusted, and `_check_unsafe`\nreturns early, so the malicious template's tasks execute with no prompt.\n\nThis is most acute on `copier update`, which reads `_src_path` from the\nproject's `.copier-answers.yml` (`copier/_subproject.py`) — i.e. an attacker who\nhands you a project controls the URL that the trust check is applied to.\n\nIn-repo asymmetry that confirms the omission: copier consistently resolves\npaths *everywhere else* it makes a security decision — `Path.resolve()` plus\n`is_relative_to(...)` guards in `_render_template`, `template_copy_root`, and\n`_external_data` — but not in the trust comparison.\n\n### PoC\n\nSelf-contained standalone script; runs against a clean, pinned PyPI install via\nthe real `copier` CLI only. **Static by default** (`copier copy --pretend`\nreaches the trust decision but does not execute tasks); `--prove-exec` is an\nopt-in supplementary run that fires an inert marker (`echo` + `touch`). The full\n`poc.py` accompanies this report.\n\nBuild and run:\n\n```bash\npython -m venv venv && . venv/bin/activate\npip install \"copier==9.15.1\"\npython poc.py                # static proof (default)\npython poc.py --prove-exec   # also fire the inert marker\n```\n\nObserved output (`copier 9.15.1`):\n\n```\n== version proof ==\n  copier == 9.15.1\n  module : .../site-packages/copier/__init__.py\n\n== inputs ==\n  trusted prefix (settings.yml): /tmp/copier_trust_poc_XXXX/trusted_templates/\n  control src (canonical)      : /tmp/copier_trust_poc_XXXX/attacker/evil_template\n  exploit src (traversal)      : /tmp/copier_trust_poc_XXXX/trusted_templates/../attacker/evil_template\n  both resolve to the SAME dir : True\n  exploit startswith trusted/  : True\n  minimal delta                : exploit = '/tmp/copier_trust_poc_XXXX/trusted_templates/..' + '/attacker/evil_template'\n\n== static proof (copier copy --pretend; payload NOT executed) ==\n  control (canonical, untrusted): exit=4  -> BLOCKED (UnsafeTemplateError)\n  exploit (trusted-prefix /..)  : exit=0  -> TRUSTED, task reached\n  marker on disk after --pretend: False (expected False: --pretend does not run tasks)\n\n  --- copier's own output for the exploit (note the task it WOULD run) ---\n  | Copying from template version None\n  |     create  hello.txt\n  |  > Running task 1 of 1: echo COPIER-TRUST-BYPASS-RCE-MARKER && touch COPIER_RCE_PROOF\n\n== VERDICT ==\n  BYPASS CONFIRMED: identical template is refused by canonical path\n  (exit 4) yet granted trust via '<trusted>/..' traversal (exit 0),\n  so its tasks run with no --trust prompt.\n\n== --prove-exec: running the exploit for real (inert marker) ==\n  | COPIER-TRUST-BYPASS-RCE-MARKER\n  |  > Running task 1 of 1: echo COPIER-TRUST-BYPASS-RCE-MARKER && touch COPIER_RCE_PROOF\n  exit=0  marker file 'COPIER_RCE_PROOF' created: True\n  -> ARBITRARY COMMAND EXECUTED via a 'trusted' template, no --trust\n```\n\nThe exploit is the same template as the control plus the minimal delta\n`<trusted_prefix>/..`. Deterministic: same input → same result. The PoC uses a\nlocal trusted prefix for a self-contained, network-free run; the `https` case is\nidentical because git normalizes `..` before the request — e.g.\n`git ls-remote \"https://github.com/copier-org/../pallets/flask.git\"` emits\n`warning: redirecting to https://github.com/pallets/flask.git/` and returns\n`pallets/flask`'s refs, a different org than the trusted `copier-org/`.\n\n### Impact\n\nA user who has configured a trusted **prefix** (a trailing-`/` entry in\n`trust`, a documented feature) no longer gets the unsafe-feature prompt for a\ntemplate that merely *appears* to live under that prefix. Any party who can\ninfluence the template URL — most realistically the author of a project the\nvictim runs `copier update` on, since `_src_path` comes from that project's\n`.copier-answers.yml` — can host the real template under a different\norg/location reached via `..` and have its `tasks`/`migrations`/\n`jinja_extensions` execute arbitrary commands with no prompt. It fires on a\ndefault, modern git for both local paths and `https`.\n\nProposed severity: **High**, comparable to the project's prior unsafe-template\nadvisory (GHSA-3xw7-v6cj-5q8h). Proposed CVSS v4 vector (maintainer to finalize;\n`AT:P` reflects the required trusted-prefix configuration):\n`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\nvariant if you scope impact to the user account only (no host escape claim):\ndrop `SC/SI/SA` to `N`.\n\n### Recommended fix\n\nNormalize **both** sides before comparing, instead of raw `startswith`. For\nlocal entries, compare resolved absolute paths (`Path(t).resolve()` vs\n`Path(repository).resolve()`) using segment containment / `is_relative_to`, the\npattern already used in `_render_template` and `template_copy_root`. For URL\nentries, parse the URL and reject or collapse `..`/`.`/empty path segments\nbefore the prefix test. As defense-in-depth, reject any `_src_path` read from an\nanswers file that contains `..` segments after the scheme/host, since\nlegitimate template URLs never need them.\n\n### References\n\n- CWE-22 — https://cwe.mitre.org/data/definitions/22.html\n- CWE-94 — https://cwe.mitre.org/data/definitions/94.html\n- Affected source (tag `v9.15.1`): `copier/_settings.py:141-146` (prefix match),\n  `copier/_settings.py:149-152` (`_normalize`), `copier/_main.py:293` (trust gate).\n- Documented prefix behavior: `docs/settings.md` (\"Locations ending with `/`\n  will be matched as prefixes\").\n- `https` `..` normalization: libcurl removes dot segments by default\n  (`CURLOPT_PATH_AS_IS` defaults to off) — https://curl.se/libcurl/c/CURLOPT_PATH_AS_IS.html\n- Novelty: distinct from copier's published advisories, which concern filesystem\n  read/write traversal in rendered output; this is an authorization bypass in\n  the `trust` setting's URL matching. The flawed match is identical between\n  released `v9.15.1` and current `master` HEAD, and unchanged since the\n  trust-prefix feature was introduced in `v9.5.0` (originally `copier/settings.py`,\n  commit `71358ed`; renamed to `copier/_settings.py` in the v9.12.0 refactor).",
  "severity" : [ {
    "type" : "CVSS_V4",
    "score" : "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"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "PyPI",
      "name" : "copier"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "9.5.0"
      }, {
        "fixed" : "9.15.2"
      } ]
    } ],
    "database_specific" : {
      "last_known_affected_version_range" : "<= 9.15.1"
    }
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/copier-org/copier/security/advisories/GHSA-9gmc-jqmh-3rvm"
  }, {
    "type" : "ADVISORY",
    "url" : "https://nvd.nist.gov/vuln/detail/CVE-2026-53951"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/copier-org/copier"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/copier-org/copier/releases/tag/v9.15.2"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-22", "CWE-94" ],
    "severity" : "HIGH",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-08-19T19:16:54Z",
    "nvd_published_at" : "2026-07-08T16:16:30Z"
  }
}