VYPR
Moderate severityOSV Advisory· Published Jan 21, 2026· Updated Jan 22, 2026

Copier safe template has arbitrary filesystem read access via symlinks when _preserve_symlinks: false

CVE-2026-23968

Description

Copier is a library and CLI app for rendering project templates. Prior to version 9.11.2, Copier suggests that it's safe to generate a project from a safe template, i.e. one that doesn't use unsafe features like custom Jinja extensions which would require passing the --UNSAFE,--trust flag. As it turns out, a safe template can currently include arbitrary files/directories outside the local template clone location by using symlinks along with _preserve_symlinks: false (which is Copier's default setting). Version 9.11.2 patches the issue.

AI Insight

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

Copier before 9.11.2 allows safe templates to include arbitrary files outside the template root via symlinks when `_preserve_symlinks` is false.

Vulnerability

Overview

Copier is a library and CLI tool for rendering project templates. Prior to version 9.11.2, Copier's documentation suggests that generating a project from a 'safe' template (one that does not use custom Jinja extensions requiring the --UNSAFE,--trust flag) is secure. However, a safe template can include arbitrary files or directories from outside the local template clone location by using symlinks combined with the default setting _preserve_symlinks: false [1][4]. This allows a malicious template author to exfiltrate sensitive data from the user's filesystem.

Exploitation

An attacker can craft a template that contains a symlink pointing to a sensitive file or directory outside the template's root. When Copier copies the template with _preserve_symlinks: false (the default), it resolves the symlink and includes the target content in the generated project. For example, a symlink named stolen-secret.txt pointing to ../secret.txt will copy the contents of secret.txt into the output [4]. No special flags or authentication are needed; the user only needs to run copier copy on the malicious template.

Impact

An attacker who can convince a user to generate a project from a malicious template can read arbitrary files from the user's filesystem, such as SSH keys, credentials, or other secrets. If the user then pushes the generated project to a public repository, the attacker can extract the stolen data [4]. This breaks the trust model of safe templates.

Mitigation

Version 9.11.2 patches the issue by disallowing symlink-based includes that resolve outside the template root [3]. Users should update to Copier 9.11.2 or later. There are no known workarounds for earlier versions [4].

AI Insight generated on May 19, 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.11.29.11.2

Affected products

1

Patches

1
b3a7b3772d17

fix: disallow symlink-based includes outside template root

https://github.com/copier-org/copierSigurd SpieckermannJan 13, 2026via ghsa
2 files changed · +92 1
  • copier/_main.py+14 0 modified
    @@ -718,6 +718,20 @@ def _render_template(self) -> None:
             dst_root = self.dst_path.resolve()
             for src in scantree(str(self.template_copy_root), follow_symlinks):
                 src_abspath = Path(src.path)
    +            # If the source is a symlink, we are not preserving symlinks, and the
    +            # symlink target is outside the template root, this means that we are
    +            # copying a file/directory from outside the template, which is
    +            # forbidden, so raise an error.
    +            if (
    +                src_abspath.is_symlink()
    +                and not self.template.preserve_symlinks
    +                and not (src_abspath.resolve()).is_relative_to(
    +                    self.template.local_abspath
    +                )
    +            ):
    +                raise ForbiddenPathError(
    +                    path=src_abspath.relative_to(self.template_copy_root)
    +                )
                 src_relpath = Path(src_abspath).relative_to(self.template.local_abspath)
                 dst_relpaths_ctxs = self._render_path(
                     Path(src_abspath).relative_to(self.template_copy_root)
    
  • tests/test_symlinks.py+78 1 modified
    @@ -1,11 +1,13 @@
     import os
    +import re
     from pathlib import Path
    +from tempfile import gettempdir
     
     import pytest
     from plumbum import local
     
     from copier import run_copy, run_update
    -from copier.errors import DirtyLocalWarning
    +from copier.errors import DirtyLocalWarning, ForbiddenPathError
     
     from .helpers import build_file_tree, git
     
    @@ -569,3 +571,78 @@ def test_symlinked_to_outside_destination_relative(
         assert (dst / "symlink_dir" / "outside.txt").read_text() == "outside"
         assert (dst / "a_symlink.txt").is_symlink()
         assert (dst / "a_symlink.txt").read_text() == "outside"
    +
    +
    +def test_resolve_relative_symlink_outside_template_root_raises_error(
    +    tmp_path_factory: pytest.TempPathFactory,
    +) -> None:
    +    src, dst, other = map(tmp_path_factory.mktemp, ("src", "dst", "other"))
    +
    +    build_file_tree(
    +        {
    +            src / ".copier-answers.yml.jinja": """\
    +            # Changes here will be overwritten by Copier
    +            {{ _copier_answers|to_nice_yaml }}
    +            """,
    +            src / "copier.yaml": """\
    +            _preserve_symlinks: false
    +            """,
    +            src / "symlink.txt": Path(
    +                # HACK: This is the path to `outside.txt` relative to  the location the
    +                # symlink file in the template's local clone location. To construct
    +                # this path dynamically, we rely on internal knowledge where Copier
    +                # clones the template.
    +                os.path.relpath(other, start=Path(gettempdir(), "template-clone")),
    +                "outside.txt",
    +            ),
    +            other / "outside.txt": "outside",
    +        }
    +    )
    +
    +    with local.cwd(src):
    +        git("init")
    +        git("add", "-A")
    +        git("commit", "-m", "init")
    +
    +    with pytest.raises(
    +        ForbiddenPathError,
    +        match=re.escape('"symlink.txt" is forbidden'),
    +    ):
    +        run_copy(str(src), dst, defaults=True, overwrite=True, cleanup_on_error=False)
    +
    +    assert not (dst / "symlink.txt").exists()
    +
    +
    +@pytest.mark.skipif(
    +    os.name == "nt", reason="Absolute paths not created as symlinks on Windows"
    +)
    +def test_resolve_absolute_symlink_outside_template_root_raises_error(
    +    tmp_path_factory: pytest.TempPathFactory,
    +) -> None:
    +    src, dst, other = map(tmp_path_factory.mktemp, ("src", "dst", "other"))
    +    build_file_tree(
    +        {
    +            src / ".copier-answers.yml.jinja": """\
    +            # Changes here will be overwritten by Copier
    +            {{ _copier_answers|to_nice_yaml }}
    +            """,
    +            src / "copier.yaml": """\
    +            _preserve_symlinks: false
    +            """,
    +            src / "symlink.txt": other / "outside.txt",
    +            other / "outside.txt": "outside",
    +        }
    +    )
    +
    +    with local.cwd(src):
    +        git("init")
    +        git("add", "-A")
    +        git("commit", "-m", "init")
    +
    +    with pytest.raises(
    +        ForbiddenPathError,
    +        match=re.escape('"symlink.txt" is forbidden'),
    +    ):
    +        run_copy(str(src), dst, defaults=True, overwrite=True)
    +
    +    assert not (dst / "symlink.txt").exists()
    

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.