VYPR
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.

PackageAffected versionsPatched versions
copierPyPI
< 9.9.19.9.1

Affected products

1

Patches

2
165c85a1536c

bump: version 9.9.0 → 9.9.1

https://github.com/copier-org/copierSigurd SpieckermannAug 18, 2025via osv
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
    
3feea3b3ff3c

fix: cast Jinja context path variables to `pathlib.PurePath`

https://github.com/copier-org/copierSigurd SpieckermannAug 13, 2025via ghsa
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"] &vert; 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

4

News mentions

0

No linked articles in our index yet.