CVE-2026-25115

Snake in the Grass: Breaking n8n's Python Sandbox via Symlink Voodoo

Amit Schendel
Amit Schendel
Senior Security Researcher

Feb 4, 2026·6 min read·3 visits

Executive Summary (TL;DR)

Critical RCE in n8n versions < 2.4.8. The Python Code node's path validation logic fails when handling files that don't exist yet, allowing attackers to use symlinks to write files outside the sandbox. This leads to full host compromise.

A critical sandbox escape vulnerability in the n8n workflow automation platform allows authenticated users to execute arbitrary code on the host system. The flaw resides in the file path canonicalization logic of the Python Code node, specifically handling non-existent files. By exploiting a fallback mechanism that relies on string manipulation rather than filesystem validation, attackers can traverse directories via symlinks, effectively breaking out of the intended security boundaries. This turns a low-privilege workflow editor into a full system administrator.

The Hook: Pandora's Python Box

Automation tools like n8n are the glue of the modern internet. They take data from point A, massage it, and shove it into point B. To make this truly powerful, n8n offers a Python Code node, effectively giving users a shell environment to run custom scripts. Ideally, this environment is a padded room—a sandbox where you can play with data but can't touch the sharp objects (the host filesystem).

However, building a perfect jail is notoriously difficult, especially when you allow users to interact with files. The security model relies on a simple promise: "You can play with files, but only inside this specific directory." It sounds simple enough. You check the path, make sure it starts with /sandbox/safe/, and let the script run. But filesystem paths are not simple strings; they are pointers to a physical reality that can be manipulated.

CVE-2026-25115 is the story of what happens when a developer trusts a string representation of a path more than the filesystem itself. It’s a classic tale of "it looked safe on paper," leading to a complete compromise of the host server. If you run n8n and allow untrusted users to create workflows, your server isn't yours anymore—it's theirs.

The Flaw: The Phantom File Fallback

The vulnerability lies in a function called resolvePath in packages/core/src/execution-engine/node-execution-context/utils/file-system-helper-functions.ts. The purpose of this function is to take a user-provided path and return its canonical, absolute form. This is the gatekeeper. If the resolved path doesn't start with the allowed sandbox root, the operation is blocked.

Here is where the logic went sideways. The code correctly used fs.realpath() for files that existed. fs.realpath() is the gold standard because it asks the OS, "Hey, where does this inode actually live?" It resolves symlinks, .. segments, and returns the hard truth.

But what if the file doesn't exist yet? For example, if the script is trying to create a new output file? The original code caught the ENOENT (Error NO ENTry) exception and fell back to path.resolve(). This was the fatal mistake. path.resolve() is a library function that performs string manipulation. It doesn't look at the disk. It doesn't know that a folder in the path is actually a symlink pointing to /root. It just cleans up the string. The gatekeeper was checking a map drawn by the attacker, not the terrain itself.

The Code: The Smoking Gun

Let's look at the specific lines that killed the security model. In the vulnerable version, the error handling explicitly downgraded the security check from a filesystem operation to a string operation.

// VULNERABLE CODE
async function resolvePath(path: PathLike): Promise<ResolvedFilePath> {
    try {
        // The safe way: ask the filesystem
        return (await fsRealpath(path)) as ResolvedFilePath;
    } catch (error: unknown) {
        // If the file is missing (ENOENT)...
        if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
            // THE BUG: Trusting the string representation
            return resolve(path.toString()) as ResolvedFilePath;
        }
        throw error;
    }
}

When the patch landed in commit 5c69970acc7d37049deae67da861f92d2aaa9b03, the fix was obvious in hindsight. If the file doesn't exist, you can't resolve it, but you must resolve its parent directory. You cannot trust any part of the path that isn't anchored to the physical disk.

// PATCHED CODE
if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
    const dir = dirname(pathStr);
    const file = basename(pathStr);
    // The fix: Force resolution of the parent directory
    const resolvedDir = await fsRealpath(dir);
    return join(resolvedDir, file) as ResolvedFilePath;
}

By forcing fsRealpath(dir), the application ensures that if dir involves a symlink escaping the sandbox, resolvedDir will reflect the true location (e.g., /etc/), causing the subsequent prefix check to fail.

The Exploit: Tunneling Out

To exploit this, we need to trick the application into thinking a file write is safe when it isn't. The strategy relies on the difference between the path string and the path reality. We will use the Python Code node to set up a trap.

Step 1: The Setup First, we use valid Python code to create a symlink inside our allowed sandbox directory. The system allows this because we aren't leaving the sandbox yet. os.symlink('/', '/sandbox/data/tunnel') Now, /sandbox/data/tunnel points to the root of the filesystem.

Step 2: The Bait We tell n8n we want to write a file to /sandbox/data/tunnel/tmp/pwned. The file doesn't exist, so fs.realpath throws ENOENT.

Step 3: The Switch The vulnerable catch block executes path.resolve('/sandbox/data/tunnel/tmp/pwned'). Since path.resolve treats this as a string, it returns exactly what we gave it. The security check looks at the string: "Does it start with /sandbox/data? Yes." The gate opens.

Step 4: Execution n8n performs the actual file write using the path /sandbox/data/tunnel/tmp/pwned. The operating system sees the symlink tunnel, resolves it to /, and writes the file to /tmp/pwned on the host. We have now achieved arbitrary file write. From here, overwriting .ssh/authorized_keys or a cron job for full RCE is trivial.

The Impact: Why You Should Panic

This is a CVSS 9.4 Critical vulnerability for a reason. n8n is often deployed with access to sensitive internal APIs, database credentials, and cloud keys. It sits at the center of an organization's automation infrastructure.

An attacker who exploits this doesn't just get to write a text file; they get the identity of the n8n process. In Docker environments, they might break out of the container next. In bare-metal installs, they own the server. They can steal all the credentials stored in n8n credential store, modify other workflows to exfiltrate data, or install persistent backdoors.

Because n8n is designed to execute code, the "Attack Complexity" is low. You don't need to bypass ASLR or craft heap sprays. You just need to write three lines of Python.

The Fix: Closing the Window

The remediation is straightforward: Update to version 2.4.8. The n8n team responded quickly and the patch effectively kills the symlink vector by verifying the parent directory of any new file.

If you cannot patch immediately, you are in a precarious position. The only effective mitigation is to disable the Python Code node entirely or restrict access to the workflow editor to only the most trusted personnel. Network segmentation won't save you here—the call is coming from inside the house.

For security researchers and developers, the lesson is clear: String manipulation is not security. If you are validating paths, you must ask the filesystem for the truth, the whole truth, and nothing but the truth.

Official Patches

Fix Analysis (1)

Technical Appendix

CVSS Score
9.4/ 10
CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H

Affected Systems

n8n (Self-hosted)n8n (Cloud)n8n (Docker)n8n (npm package)

Affected Versions Detail

Product
Affected Versions
Fixed Version
n8n
n8n.io
< 2.4.82.4.8
AttributeDetail
CWE IDCWE-693
Attack VectorNetwork (Authenticated)
CVSS Score9.4 (Critical)
ImpactRemote Code Execution (RCE) / Sandbox Escape
Patch Commit5c69970acc7d37049deae67da861f92d2aaa9b03
Exploit StatusPoC Available (Theoretical)
CWE-693
Protection Mechanism Failure

Protection Mechanism Failure

Vulnerability Timeline

Patch committed to n8n repository
2026-01-13
Public disclosure via GitHub Advisory
2026-02-04
CVE Assigned and Rated Critical
2026-02-04

Subscribe to updates

Get the latest CVE analysis reports delivered to your inbox.