Symlink Sabotage: Exfiltrating Secrets via Copier Templates
Jan 22, 2026·5 min read·3 visits
Executive Summary (TL;DR)
Prior to version 9.11.2, Copier failed to validate the destination of symbolic links within templates. If a user generated a project from a malicious template, the engine would blindly follow symlinks pointing outside the template directory (e.g., to `/etc/passwd` or `~/.ssh/id_rsa`), read their contents, and write them into the new project. If the user then pushed this project to a public repo, their secrets were exposed.
A high-severity path traversal vulnerability in the popular Python templating tool 'Copier' allowed malicious templates to access and replicate sensitive files from the victim's host machine via symbolic links.
The Hook: Trusting the Blueprint
In the world of modern development, we are lazy. And I mean that in the best way possible. We don't write boilerplate code; we use scaffolding tools. Enter Copier, a beloved Python library that takes a template repository and spits out a shiny new project structure for you. It’s smart, it handles updates, and it separates templates into "safe" and "unsafe" categories.
The "unsafe" ones run arbitrary Python or shell code. The "safe" ones are just supposed to copy files and render Jinja text. You, the security-conscious developer, assume that running a "safe" template is akin to browsing a static website—boring and harmless.
But here is the catch: CVE-2026-23968 turns that assumption on its head. It turns out that a "safe" template could be weaponized to treat your local filesystem like an open buffet, grabbing your SSH keys, AWS credentials, or shadow files, and politely serving them up in the generated project directory. It’s not remote code execution; it’s authorized theft.
The Flaw: The Symlink Slide
The vulnerability stems from a classic logical oversight: implicit trust in file system objects. Copier has a setting called _preserve_symlinks, which defaults to false. When this is false, Copier acts like a diligent scribe. If it encounters a symbolic link in the template, it doesn't copy the link itself; it follows the link to its destination, reads the content, and writes that content to the new file.
Here is the logic failure: Copier checked what to copy, but it didn't check where it was copying from. It blindly assumed that anything reachable from inside the template directory belonged to the template.
This is a CWE-61 (UNIX Symbolic Link Following) vulnerability. The engine essentially said, "Oh, you want me to copy this file named config? Sure!" ignoring the fact that config was actually a symlink pointing to /home/user/.aws/credentials. The application became a "confused deputy," using its read permissions (which are your read permissions) to access data you never intended to share.
The Code: Adding Boundaries
The fix implementation is a textbook example of "look before you leap." The maintainers had to introduce a jail check. They couldn't just stop following symlinks (that breaks functionality), so they had to ensure the resolved path of the symlink still resided within the template's territory.
Here is the logic that was introduced in copier/_main.py. Note the use of pathlib's resolve() and is_relative_to() methods:
# The Fix Logic
if (
src_abspath.is_symlink()
and not self.template.preserve_symlinks # If we are supposed to resolve links...
and not (src_abspath.resolve()).is_relative_to(
self.template.local_abspath # ...make sure the target is inside the jail.
)
):
# If the target is outside, panic.
raise ForbiddenPathError(
path=src_abspath.relative_to(self.template_copy_root)
)Before this patch, src_abspath was passed directly to the copy function. Now, the code explicitly calculates the canonical path (stripping away .. and resolving links) and asserts that this path is a child of self.template.local_abspath. If a symlink tries to escape the sandbox, the ForbiddenPathError acts as the perimeter fence.
The Exploit: The Passive Trap
Exploiting this requires zero coding skills and basic knowledge of Linux commands. An attacker doesn't need to overflow a buffer; they just need ln -s.
The Setup:
- The attacker creates a malicious repository on GitHub.
- Inside the repo, they run:
ln -s ~/.ssh/id_rsa my_key.pem. - They commit this symlink and push it.
The Trap: The attacker now social engineers a victim. "Hey, check out this cool template for a secure API starter kit!"
The Execution: The victim runs:
copier copy gh:attacker/awesome-template ./my-new-projectCopier clones the repo. It sees my_key.pem. It notices _preserve_symlinks is false. It follows the link on the victim's machine, which points to ~/.ssh/id_rsa. It reads the victim's private key. It writes the key data into ./my-new-project/my_key.pem.
The Prestige:
The victim, unaware that my_key.pem contains their actual private key (perhaps thinking it's a dummy file provided by the template), initializes a git repo and pushes ./my-new-project to GitHub. The attacker (and everyone else) now has the victim's private key.
The Impact: Silent Exfiltration
This vulnerability is particularly nasty because it is silent. There are no crashes, no strange permission errors, and no pop-ups. The file generation looks perfectly normal.
The impact is high confidentiality loss (VC:H). We aren't just talking about system configuration files. We are talking about:
- Cloud Credentials:
~/.aws/credentials,~/.azure/accessTokens.json. - SSH Keys:
~/.ssh/id_rsa. - Environment Variables:
.envfiles in parent directories. - Kubeconfigs:
~/.kube/config.
Because Copier is a developer tool, it is almost exclusively run by users with access to high-value source code and infrastructure credentials. The blast radius of a single successful exploit could be an entire organization's cloud infrastructure.
The Fix: Upgrade and Isolate
The primary mitigation is straightforward: Update Copier to version 9.11.2 or later immediately. The patch is robust and enforces filesystem boundaries strictly.
If you cannot update for some reason (perhaps you are pinned to a legacy version for compatibility), you have two options:
- Configuration: Set
_preserve_symlinks: truein your template configuration. This forces Copier to copy the symlink as a symlink rather than resolving it. The file in the output directory will still point to~/.ssh/id_rsa, but it won't contain the data. It will just be a broken link on any other machine. - Containerization: Run template generation inside a Docker container. If Copier tries to steal
/etc/shadow, it will only get the shadow file of the container, not your host machine. This is general good hygiene for running any third-party code generators.
[!NOTE] Always verify the contents of generated projects before pushing them to public repositories. A
git diffcan save your career.
Official Patches
Fix Analysis (2)
Technical Appendix
CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:P/VC:H/VI:N/VA:N/SC:N/SI:N/SA:NAffected Systems
Affected Versions Detail
| Product | Affected Versions | Fixed Version |
|---|---|---|
copier copier-org | < 9.11.2 | 9.11.2 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-61 (Symlink Following) |
| CVSS v4.0 | 6.8 (Medium) |
| Attack Vector | Local (User-Assisted) |
| Privileges Required | None |
| Impact | High Confidentiality Loss |
| Exploit Status | Proof of Concept Available |
MITRE ATT&CK Mapping
The product does not properly check if a symbolic link points to a file that is outside of the intended directory.
Known Exploits & Detection
Vulnerability Timeline
Subscribe to updates
Get the latest CVE analysis reports delivered to your inbox.