Arbitrary File Write | decompress
Summary
Arbitrary File Write in decompress via symlink duplicate entry.
When extracting a ZIP archive containing two entries with the same path — the first being a symlink to an arbitrary target and the second being a regular file — the file content is written through the symlink to a location outside the output directory. This bypasses all existing path-traversal protections, including preventWritingThroughSymlink.
The full narrative of how this becomes RCE in AnythingLLM is in the blog post: From a document upload to RCE in AnythingLLM.
Affected packages
decompress(npm) — tested on 4.2.1, all versions likely affected.- Downstream, anything in the dependency tree that extracts user-supplied archives, including:
officeparser(npm, 4.x) — usesdecompressto extract PPTX/DOCX/XLSX/ODT/ODP/ODS.anythingllm— usesofficeparserto parse uploaded documents → RCE via a normal file upload.
This list is not exhaustive.
Root cause
Archive entries are processed in parallel via Promise.all (source). The preventWritingThroughSymlink check uses an async readlink call that resolves before the concurrent symlink entry has been created on disk.
Deterministic ordering due to Node.js microtask scheduling:
Promise B (file): readlink("output/data.txt") → not found → null → "safe"
Promise A (symlink): symlink("/etc/target" → "output/data.txt") → created
Promise B (file): writeFile("output/data.txt", payload) → follows symlink → writes to /etc/target
The readlink check in Promise B always resolves before Promise A creates the symlink, but the writeFile in Promise B always runs after it — so the write follows the freshly-created symlink.
Impact
Any application calling decompress(untrustedArchive, outputDir) is vulnerable to arbitrary file write on the host filesystem, which can escalate to remote code execution depending on the environment.
Escalating a bare arbitrary write
When the target does not expose a file that is later executed, the write primitive can still be escalated. Sonar’s Why code security matters even in hardened environments describes corrupting internal libuv state (a uv_signal_s handle) to pivot into a ROP chain calling execve. A Node 24 proof of concept is available at poc_node24.py.
Proof of Concept
- Minimal arbitrary-file-write PoC and archive generator.
- Full AnythingLLM RCE PoC (
.pptxgenerator).
See the repository: github.com/Alemmi/cve/tree/main/CVE-2026-10732