High severityOSV Advisory· Published Aug 18, 2025· Updated Apr 15, 2026
CVE-2025-55201
CVE-2025-55201
Description
Copier library and CLI app for rendering project templates. Prior to 9.9.1, a safe template can currently read and write arbitrary files because Copier exposes a few pathlib.Path objects in the Jinja context which have unconstrained I/O methods. This effectively renders the security model w.r.t. filesystem access useless. This vulnerability is fixed in 9.9.1.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
copierPyPI | < 9.9.1 | 9.9.1 |
Affected products
1- Range: 1.1.1, 3.0.0-alpha5, 3.0.0-alpha6, …
Patches
2165c85a1536cbump: version 9.9.0 → 9.9.1
2 files changed · +8 −1
CHANGELOG.md+7 −0 modified@@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file. This projec adheres to [PEP 440](https://www.python.org/dev/peps/pep-0440/) versioning schema, and the changelog itself conforms to [Keep A Changelog](https://keepachangelog.com/). +## v9.9.1 (2025-08-18) + +### Security + +- disallow render paths outside destination directory +- cast Jinja context path variables to `pathlib.PurePath` + ## v9.9.0 (2025-08-01) ### Feat
pyproject.toml+1 −1 modified@@ -179,7 +179,7 @@ annotated_tag = true changelog_incremental = true tag_format = "v$version" update_changelog_on_bump = true -version = "9.9.0" +version = "9.9.1" [tool.codespell] # Ref: https://github.com/codespell-project/codespell#using-a-config-file
3feea3b3ff3cfix: cast Jinja context path variables to `pathlib.PurePath`
3 files changed · +72 −11
copier/_main.py+13 −8 modified@@ -13,7 +13,7 @@ from filecmp import dircmp from functools import cached_property, partial, wraps from itertools import chain -from pathlib import Path +from pathlib import Path, PurePath from shutil import rmtree from tempfile import TemporaryDirectory from types import TracebackType @@ -399,9 +399,9 @@ def _render_context(self) -> AnyByStrMutableMapping: """Produce render context for Jinja.""" conf = LazyDict( { - "src_path": lambda: self.template.local_abspath, - "dst_path": lambda: self.dst_path, - "answers_file": lambda: self.answers_relpath, + "src_path": lambda: PurePath(self.template.local_abspath), + "dst_path": lambda: PurePath(self.dst_path), + "answers_file": lambda: PurePath(self.answers_relpath), "vcs_ref": lambda: self.resolved_vcs_ref, "vcs_ref_hash": lambda: self.template.commit_hash, "data": lambda: self.data, @@ -659,13 +659,18 @@ def jinja_env(self) -> YieldEnvironment: "Make sure to install these extensions alongside Copier itself.\n" "See the docs at https://copier.readthedocs.io/en/latest/configuring/#jinja_extensions" ) + + def to_json_fallback(value: Any) -> Any: + if isinstance(value, LazyDict): + return dict(value) + if isinstance(value, PurePath): + return str(value) + return value + # patch the `to_json` filter to support Pydantic dataclasses env.filters["to_json"] = partial( env.filters["to_json"], - default=partial( - to_jsonable_python, - fallback=lambda v: dict(v) if isinstance(v, LazyDict) else v, - ), + default=partial(to_jsonable_python, fallback=to_json_fallback), ) # Add a global function to join filesystem paths.
docs/creating.md+3 −3 modified@@ -106,13 +106,13 @@ Attributes: | Name | Type | Description | | ------------------ | ------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `answers_file` | `Path` | The path for [the answers file][the-copier-answersyml-file] relative to `dst_path`.<br>See the [`answers_file`][] setting for related information. | +| `answers_file` | `PurePath` | The path for [the answers file][the-copier-answersyml-file] relative to `dst_path`.<br>See the [`answers_file`][] setting for related information. | | `cleanup_on_error` | `bool` | When `True`, delete `dst_path` if there's an error.<br>See the [`cleanup_on_error`][] setting for related information. | | `conflict` | `Literal["inline", "rej"]` | The output format of a diff code hunk when [updating][updating-a-project] a file yields conflicts.<br>See the [`conflict`][] setting for related information. | | `context_lines` | `PositiveInt` | Lines of context to consider when solving conflicts in updates.<br>See the [`context_lines`][] setting for related information. | | `data` | `dict[str, Any]` | Answers to the questionnaire, defined in the template, provided via CLI (`-d,--data`) or API (`data`).<br>See the [`data`][] setting for related information.<br>⚠️ May contain secret answers. | | `defaults` | `bool` | When `True`, use default answers to questions.<br>See the [`defaults`][] setting for related information. | -| `dst_path` | `Path` | Destination path where to render the subproject.<br>⚠️ When [updating a project][updating-a-project], it may be a temporary directory, as Copier's update algorithm generates fresh copies using the old and new template versions in temporary locations. | +| `dst_path` | `PurePath` | Destination path where to render the subproject.<br>⚠️ When [updating a project][updating-a-project], it may be a temporary directory, as Copier's update algorithm generates fresh copies using the old and new template versions in temporary locations. | | `exclude` | `Sequence[str]` | Specified additional [file exclusion patterns][patterns-syntax].<br>See the [`exclude`][] setting for related information. | | `os` | <code>Literal["linux", "macos", "windows"] | None</code> | The detected operating system, `None` if it could not be detected. | | `overwrite` | `bool` | When `True`, overwrite files that already exist, without asking.<br>See the [`overwrite`][] setting for related information. | @@ -123,7 +123,7 @@ Attributes: | `skip_answered` | `bool` | When `True`, skip questions that have already been answered.<br>See the [`skip_answered`][] setting for related information. | | `skip_if_exists` | `Sequence[str]` | Specified additional [file skip patterns][patterns-syntax].<br>See the [`skip_if_exists`][] setting for related information. | | `skip_tasks` | `bool` | When `True`, skip [template tasks execution][tasks].<br>See the [`skip_tasks`][] setting for related information. | -| `src_path` | `Path` | The absolute path to the (cloned/downloaded) template on disk. | +| `src_path` | `PurePath` | The absolute path to the (cloned/downloaded) template on disk. | | `unsafe` | `bool` | When `True`, allow usage of unsafe templates.<br>See the [`unsafe`][] setting for related information. | | `use_prereleases` | `bool` | When `True`, `vcs_ref`/`vcs_ref_hash` may refer to a prerelease version of the template.<br>See the [`use_prereleases`][] setting for related information. | | `user_defaults` | `dict[str, Any]` | Specified user defaults that may override a template's defaults during question prompts. |
tests/test_context.py+56 −0 modified@@ -1,5 +1,7 @@ import json +import sys from pathlib import Path +from uuid import uuid4 import pytest from plumbum import local @@ -9,6 +11,60 @@ from .helpers import build_file_tree, git_save +def test_no_path_variables( + tmp_path_factory: pytest.TempPathFactory, monkeypatch: pytest.MonkeyPatch +) -> None: + """Test that there are no context variables of type `pathlib.Path`.""" + src, dst = map(tmp_path_factory.mktemp, ("src", "dst")) + ext_module_name = uuid4() + build_file_tree( + { + src / f"{ext_module_name}.py": ( + """\ + from pathlib import Path + from typing import Any, Mapping + + from jinja2 import Environment, pass_context + from jinja2.ext import Extension + from jinja2.runtime import Context + from pydantic import BaseModel + + + class ContextExtension(Extension): + def __init__(self, environment: Environment) -> None: + super().__init__(environment) + environment.globals["__assert"] = self._assert + + @pass_context + def _assert(self, ctx: Context) -> None: + items: list[tuple[str, Any]] = list(dict(ctx).items()) + for k, v in items: + if isinstance(v, Path): + raise AssertionError( + f"{k} must not be a `pathlib.Path` object" + ) + if isinstance(v, BaseModel): + v = dict(v) + if isinstance(v, Mapping): + items.extend((f"{k}.{k2}", v2) for k2, v2 in v.items()) + elif isinstance(v, (list, tuple, set)): + items.extend((f"{k}[{i}]", v2) for i, v2 in enumerate(v)) + """ + ), + src / "copier.yml": ( + f"""\ + _jinja_extensions: + - {ext_module_name}.ContextExtension + """ + ), + src / "test.txt.jinja": "{{ __assert() | default('', true) }}", + } + ) + monkeypatch.setattr("sys.path", [str(src), *sys.path]) + copier.run_copy(str(src), dst, unsafe=True) + assert (dst / "test.txt").read_text("utf-8") == "" + + def test_exclude_templating_with_operation( tmp_path_factory: pytest.TempPathFactory, ) -> None:
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
4News mentions
0No linked articles in our index yet.