VYPR
Medium severity4.4NVD Advisory· Published Apr 2, 2026· Updated Apr 3, 2026

CVE-2026-34726

CVE-2026-34726

Description

Copier is a library and CLI app for rendering project templates. Prior to version 9.14.1, Copier's _subdirectory setting is documented as the subdirectory to use as the template root. However, the current implementation accepts parent-directory traversal such as .. and uses it directly when selecting the template root. As a result, a template can escape its own directory and make Copier render files from the parent directory without --UNSAFE. This issue has been patched in version 9.14.1.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Copier prior to 9.14.1 allowed template root escape via parent-directory traversal in _subdirectory, enabling unauthorized file rendering.

Vulnerability

Copier, a library and CLI tool for rendering project templates, contains a path traversal vulnerability in its _subdirectory setting. The _subdirectory field is intended to specify a subdirectory within the template to use as the root for rendering. However, the implementation accepts parent-directory traversal sequences such as .. and directly concatenates the rendered string with the template's local absolute path via self.template.local_abspath / subdir. This allows a template to escape its own directory and make Copier render files from parent directories without requiring the --UNSAFE flag [1].

Exploitation

An attacker can craft a malicious template that sets _subdirectory to a value like ../forbidden or an absolute path pointing outside the template root. When Copier processes the template, it resolves the path and walks that directory as the template root, effectively copying files from arbitrary locations on the filesystem. No special authentication or network position is required beyond the ability to supply a template to Copier (e.g., via a Git URL or local path) [1][4].

Impact

Successful exploitation allows an attacker to read and render files from directories outside the intended template root. This could lead to disclosure of sensitive information, such as configuration files or source code, that the template author did not intend to expose. The vulnerability bypasses the safety mechanism (--UNSAFE) that normally prevents such behavior [1].

Mitigation

The issue has been patched in Copier version 9.14.1. The fix introduces validation that rejects _subdirectory paths that resolve outside the template root, raising a ForbiddenPathError [4]. Users should upgrade to version 9.14.1 or later. No workaround is available for earlier versions [1].

AI Insight generated on May 18, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
copierPyPI
< 9.14.19.14.1

Affected products

1

Patches

1
cb80a3ffc9c3

fix: disallow `_subdirectory` path outside template root

https://github.com/copier-org/copierSigurd SpieckermannMar 30, 2026via ghsa
2 files changed · +71 1
  • copier/_main.py+4 1 modified
    @@ -1059,7 +1059,10 @@ def template_copy_root(self) -> Path:
             It points to the cloned template local abspath + the rendered subdir, if any.
             """
             subdir = self._render_string(self.template.subdirectory) or ""
    -        return self.template.local_abspath / subdir
    +        path = (self.template.local_abspath / subdir).resolve()
    +        if not path.is_relative_to(self.template.local_abspath):
    +            raise ForbiddenPathError(path=Path(subdir))
    +        return path
     
         # Main operations
         @as_operation("copy")
    
  • tests/test_subdirectory.py+67 0 modified
    @@ -7,6 +7,7 @@
     
     import copier
     from copier._user_data import load_answersfile_data
    +from copier.errors import ForbiddenPathError
     
     from .helpers import BRACKET_ENVOPS_JSON, SUFFIX_TMPL, build_file_tree, git, git_init
     
    @@ -333,3 +334,69 @@ def test_new_version_changes_subdirectory(
         # Also assert the subdirectories themselves were not rendered
         assert not (dst / "subdir1").exists()
         assert not (dst / "subdir2").exists()
    +
    +
    +@pytest.mark.parametrize("is_absolute", [False, True])
    +def test_subdirectory_path_outside_template_root_on_copy_raises_error(
    +    tmp_path_factory: pytest.TempPathFactory,
    +    is_absolute: bool,
    +) -> None:
    +    src, dst = map(tmp_path_factory.mktemp, ("src", "dst"))
    +    build_file_tree(
    +        {
    +            src / "template" / "copier.yml": (
    +                f"_subdirectory: {src / 'forbidden' if is_absolute else '../forbidden'}"
    +            ),
    +            src / "forbidden" / "forbidden.txt": "forbidden",
    +        }
    +    )
    +    with pytest.raises(ForbiddenPathError, match="forbidden"):
    +        copier.run_copy(str(src / "template"), dst)
    +
    +
    +@pytest.mark.parametrize("is_absolute", [False, True])
    +def test_subdirectory_path_outside_template_root_on_update_raises_error(
    +    tmp_path_factory: pytest.TempPathFactory,
    +    is_absolute: bool,
    +) -> None:
    +    src, dst = map(tmp_path_factory.mktemp, ("src", "dst"))
    +    build_file_tree(
    +        {
    +            src / "template" / "copier.yml": "_subdirectory: subdir",
    +            src / "template" / "subdir" / "{{ _copier_conf.answers_file }}.jinja": (
    +                "{{ _copier_answers | to_nice_yaml }}"
    +            ),
    +            src / "template" / "subdir" / "file.txt": "v1 contents",
    +        }
    +    )
    +    with local.cwd(src / "template"):
    +        git("init")
    +        git("add", ".")
    +        git("commit", "-m", "v1")
    +        git("tag", "v1")
    +
    +    copier.run_copy(str(src / "template"), dst)
    +    assert load_answersfile_data(dst).get("_commit") == "v1"
    +    assert (dst / "file.txt").exists()
    +    assert (dst / "file.txt").read_text() == "v1 contents"
    +
    +    with local.cwd(dst):
    +        git("init")
    +        git("add", ".")
    +        git("commit", "-m", "v1")
    +
    +    build_file_tree(
    +        {
    +            src / "template" / "copier.yml": (
    +                f"_subdirectory: {src / 'forbidden' if is_absolute else '../forbidden'}"
    +            ),
    +            src / "forbidden" / "forbidden.txt": "forbidden",
    +        }
    +    )
    +    with local.cwd(src / "template"):
    +        git("add", ".")
    +        git("commit", "-m", "v2")
    +        git("tag", "v2")
    +
    +    with pytest.raises(ForbiddenPathError, match="forbidden"):
    +        copier.run_update(dst, overwrite=True)
    

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

5

News mentions

1