CVE-2026-24047

Backstage Pass: Breaking Out of the Sandbox with Symlinks

Amit Schendel
Amit Schendel
Senior Security Researcher

Jan 22, 2026·7 min read·0 visits

Executive Summary (TL;DR)

CVE-2026-24047 is a path traversal vulnerability in Backstage's `resolveSafeChildPath` function. It arises from improper validation of symbolic links when the target file does not yet exist. Attackers can chain symlinks to escape the intended directory boundary, potentially leading to Remote Code Execution (RCE) by overwriting configuration files or injecting malicious scripts.

A logic flaw in the Backstage framework's path resolution utility allowed attackers to bypass sandbox restrictions using symlinks to non-existent files. By exploiting how the system handled 'phantom' paths, malicious actors could escape the Scaffolder workspace and write files to arbitrary locations on the host filesystem.

The Hook: When Is a Path Not a Path?

Backstage is the darling of Platform Engineering. It's the "portal of portals," designed to unify your infrastructure tooling, services, and documentation into one slick UI. But under the hood, Backstage is doing a lot of heavy lifting—specifically via its Scaffolder. The Scaffolder is essentially a glorified remote file manipulation engine. It clones repos, templating files, moves things around, and pushes code. That sounds a lot like "RCE as a Service" if you aren't careful.

To keep this beast in a cage, Backstage relies on a utility function called resolveSafeChildPath. Its job is simple: ensure that whatever file operations the Scaffolder performs stay strictly within a temporary workspace. It's the bouncer at the club door, checking IDs to make sure no one sneaks into the VIP section (your /etc/shadow file or app-config.yaml).

But here's the thing about filesystems: they aren't just trees; they are graphs. Thanks to symbolic links (symlinks), a path like ./workspace/subdir can instantly teleport you to /root. CVE-2026-24047 is the story of how the bouncer got tricked by a ghost—specifically, how the system failed to validate paths that didn't exist yet.

The Flaw: Trusting the Non-Existent

The vulnerability lives in the gap between intention and reality. When you want to write a file, you usually check if the destination is safe before you write it. Backstage used Node.js's fs.realpathSync to resolve paths to their absolute location, ensuring they started with the safe base directory string.

This works great for files that exist. fs.realpathSync('/safe/base/../../etc/passwd') correctly resolves to /etc/passwd, and the check fails. But what happens if you try to resolve a path for a file you are about to create?

If you ask realpath to resolve /safe/base/symlink_to_root/new_file.txt, and new_file.txt doesn't exist yet, standard implementations often throw an ENOENT error or return the path partially resolved. Backstage's implementation failed to account for dangling symlinks or symlink chains where the final segment is missing.

Because the code couldn't "see" the final file (it wasn't there yet), it essentially shrugged and assumed the path was safe as long as the string manipulation looked okay. This is a classic Time-of-Check to Time-of-Use (TOCTOU) adjacent flaw. The code assumed that if it couldn't prove the path was bad, it must be good. In security, that is a fatal assumption.

The Code: The Smoking Gun

Let's look at the logic. The vulnerable code relied too heavily on the happy path of fs.realpathSync. When an ENOENT (File Not Found) error occurred, the validation logic became permissive or incomplete. It failed to walk up the directory tree to verify the parents.

Here is the corrected logic introduced in the patch (Commit ae4dd5d1). Notice the paranoia level has increased significantly. Instead of giving up when a file is missing, it recursively climbs the directory tree until it finds firm ground.

// The Fix: Recursive Resolution
function resolveRealPath(path: string): string {
  try {
    // 1. Try the standard resolution first
    return realpathSync(path);
  } catch (ex) {
    if (ex.code !== 'ENOENT') {
      throw ex;
    }
  }
 
  // 2. ALERT: The path doesn't exist. 
  // Check if the path ITSELF is a dangling symlink.
  try {
    if (lstatSync(path).isSymbolicLink()) {
      const target = resolvePath(dirname(path), readlinkSync(path));
      // RECURSION: Follow the white rabbit.
      return resolveRealPath(target);
    }
  } catch (ex) { /* ignore */ }
 
  // 3. The file is truly missing. 
  // We must verify the PARENT directory is safe.
  const parent = dirname(path);
  if (parent === path) return path; // Hit root
 
  // RECURSION: Resolve the parent, then append the missing child.
  return resolvePath(resolveRealPath(parent), basename(path));
}

[!NOTE] The Fix Strategy: The key change is that ENOENT is no longer an exit strategy. If the leaf node is missing, the code recursively resolves the parent. If parent turns out to be a symlink to /etc, resolveRealPath(parent) will return /etc, and the final check isChildPath('/safe/base', '/etc/new_file') will correctly return false.

The Exploit: Escaping the Matrix

To exploit this, we need to create a situation where we are writing to a file through a symlink, but the file doesn't exist yet so the check passes.

The Attack Scenario

Imagine we have a malicious Scaffolder template. Templates allow us to run shell scripts or file operations. We don't need root; we just need the ability to define a template.

Step 1: The Setup We define a step in our template that creates a symbolic link. This link points to a sensitive directory outside the workspace, like the application's config directory.

# Inside the workspace
ln -s /app/backstage-backend/ malicious_tunnel

Step 2: The Bypass Now, we instruct the Scaffolder to write a file through that tunnel. We ask it to create malicious_tunnel/pwned-config.yaml.

Step 3: The Execution

  1. Backstage calls resolveSafeChildPath('./malicious_tunnel/pwned-config.yaml').
  2. realpath looks for pwned-config.yaml. It's not there.
  3. The vulnerable code catches the ENOENT and fails to fully resolve malicious_tunnel because it was only looking at the full path's existence.
  4. The check passes: "Looks like a valid path inside the workspace!"
  5. The Scaffolder writes the file.
  6. Result: The file is actually written to /app/backstage-backend/pwned-config.yaml.

The Impact: Why Should You Care?

While the CVSS score is a "Medium" 6.3, don't let that fool you. In the right environment, this is a Critical issue. The CVSS metrics often underestimate the impact of file writes in developer tooling.

Configuration Overwrite: The most direct path to total compromise is overwriting app-config.yaml. An attacker could inject a new auth provider (e.g., allowing "guest" access) or change the database connection string to point to a malicious server to steal credentials.

RCE Potential: If the Backstage instance is running scheduled tasks (cron) or using dynamic imports, writing a file to the right place equates to Remote Code Execution. For example, overwriting a script in node_modules or a CI/CD pipeline definition could grant persistent access.

Data Corruption: Even without RCE, an attacker could simply trash the system by overwriting critical system files, causing a Denial of Service. The only thing saving most deployments is that Backstage is typically run as a non-root user (hopefully).

The Fix: Closing the Door

The remediation is straightforward: Update immediately.

Patch Details

  • Component: @backstage/backend-plugin-api
  • Fixed Version: 0.1.17

If you are using @backstage/cli-common directly in your own custom plugins to validate paths, ensure you have pulled the latest version containing commit ae4dd5d.

Defense in Depth

Don't just rely on the patch. This vulnerability highlights why application-level sandboxing is fragile.

  1. Container Hardening: Ensure your Backstage container runs with a Read-Only Root Filesystem, mounting only specific temporary directories as writable.
  2. User Permissions: Restrict who can register new templates in the Scaffolder. If guest users can run arbitrary templates, you are asking for trouble.
  3. Least Privilege: Never run Backstage as root. Ensure the OS user has write access only to the specific directories it needs.

Fix Analysis (1)

Technical Appendix

CVSS Score
6.3/ 10
CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:N/A:N
EPSS Probability
0.04%
Top 100% most exploited

Affected Systems

Backstage ScaffolderBackstage Backend Plugins using `resolveSafeChildPath`Node.js applications using `@backstage/cli-common` for path validation

Affected Versions Detail

Product
Affected Versions
Fixed Version
@backstage/backend-plugin-api
Backstage
< 0.1.170.1.17
@backstage/cli-common
Backstage
< patched versionCommit ae4dd5d
AttributeDetail
CWE IDCWE-59 (Link Following)
CVSS v3.16.3 (Medium)
Attack VectorNetwork (via Scaffolder Templates)
ImpactArbitrary File Write / Potential RCE
Affected ComponentresolveSafeChildPath
Fix Commitae4dd5d1572a4f639e1a466fd982656b50f8e692
CWE-59
Link Following

Improper Link Resolution Before File Access ('Link Following')

Vulnerability Timeline

Fix committed to master
2026-01-20
CVE Published / Advisory Released
2026-01-21

Subscribe to updates

Get the latest CVE analysis reports delivered to your inbox.