Uncovering the Blind Spot: Bypassing a Security Patch (CVE-2026–24884) to Achieve Arbitrary File…
Press enter or click to view image in full sizeBug hunting is rarely about running an automated scan 2026-6-2 05:5:56 Author: infosecwriteups.com(查看原文) 阅读量:11 收藏

Sachin Patil

Press enter or click to view image in full size

Bug hunting is rarely about running an automated scanner and waiting for a critical alert. More often than not, it’s about staring at a piece of code that has already been “fixed,” brewing another cup of coffee, and asking yourself: “What did the developers assume here that isn’t actually true?”

Recently, my co-researcher Amol (@iamolofficial) and I found ourselves deep in the source code of compressinga highly popular Node.js extraction library. Our investigation led to the discovery of a complete patch bypass for a previous vulnerability (CVE-2026-24884), resulting in a new High Arbitrary File Write vulnerability, officially tracked as CVE-2026-40931.

Here is the story of how relying on string manipulation for filesystem security created a massive blind spot, and how directory poisoning can silently compromise modern CI/CD pipelines.

The Backstory: A “Fixed” Vulnerability

A few weeks ago, the library maintainers patched CVE-2026–24884, an issue where malicious symbolic links inside an archive could allow an attacker to write files outside the intended extraction directory.

The developers implemented a fix that seemed perfectly logical on paper. They added a utility function to validate that the final extraction path stayed strictly within the boundaries of the target destination folder. To do this, they used Node.js’s built-in path.resolve() to compare the destination string with the incoming file path string.

To understand the flaw, look at the original validation logic:

// The Vulnerable Logic: Trusting the String
function isPathWithinParent(childPath, parentPath) {
const normalizedChild = path.resolve(childPath);
const normalizedParent = path.resolve(parentPath);

// Only checks if the string starts with the parent directory path
return normalizedChild.startsWith(parentWithSep);
}

If the incoming file resolved to a string that started with the destination directory, it was deemed safe. The extraction would proceed. We spent hours staring at this specific validation block. It mathematically made sense. But in security research, the map is not the territory. We realized the developers were securing the map (the string), but ignoring the territory (the actual hard drive).

The “Aha!” Moment: Logical vs. Physical Divergence

The struggle with source code review is that you have to think like an Operating System.

Node’s path.resolve() is purely a logical string manipulator. It calculates paths based on rules, but it never actually looks at the disk. It has no idea if a folder actually exists, or more importantly, if a folder is secretly a symbolic link pointing somewhere else.

We formulated a hypothesis: What if the attacker doesn’t put the symlink inside the archive? What if the attacker plants a symlink on the victim’s machine before the archive is extracted?

Press enter or click to view image in full size

Figure 1:Sequence diagram showing the divergence between logical path validation and physical filesystem resolution

The Threat Model: Supply Chain and Directory Poisoning

But how does an attacker magically pre-plant a symlink on a victim’s machine? This is where the modern Supply Chain attack comes into play. We built a threat model around Directory Poisoning via Git.

Git naturally supports and tracks symbolic links. The setup for an attacker is frighteningly simple. They just need to create a repository with a poisoned symlink pointing to a sensitive target:

# Attacker's machine: Creating the trap
mkdir malicious-repo && cd malicious-repo
git init

# Create a symlink pointing outside the working directory (e.g., to a system file)
ln -s /etc/passwd config_file

# Push to GitHub
git add config_file
git commit -m "Add configuration file"

Press enter or click to view image in full size

Figure 2: The Attack Lifecycle of Git-Delivered Directory Poisoning

When a developer or an automated CI/CD pipeline runs git clone on this untrusted repository, Git natively creates the malicious symlink (e.g., config_file) on the disk. Later, the application or build script uses the compressing library to extract a seemingly harmless tarball into that newly cloned directory.

For the trap to spring, this tarball needs to contain a file entry whose name exactly matches the symlink that Git just created (e.g., a file named config_file containing the malicious payload).

When the vulnerability triggers, the library checks the path string, says “Looks safe to me!”, and calls fs.writeFile() on config_file. The underlying operating system then transparently routes the write operation through the Git-created symlink, silently overwriting sensitive system files outside the extraction root. Zero prior access required—Git unknowingly does the setup for the attacker.

Press enter or click to view image in full size

Figure 3: Successful directory poisoning demonstrating the overwrite of the target file outside the extraction root.

The Fix and a Massive Shoutout to the Maintainers

Proving a bypass is only half the job; helping secure the ecosystem is the ultimate goal. We drafted a comprehensive report detailing the “Logical vs. Physical” divergence and responsibly disclosed it.

Get Sachin Patil’s stories in your inbox

Join Medium for free to get updates from this writer.

Remember me for faster sign in

A vulnerability disclosure is only as successful as the team receiving it. We want to take a moment to give a massive shoutout to the compressing development team. In the open-source world, maintainers are often overwhelmed and under-resourced, but this team was exceptionally active, supportive, and highly professional.

Within mere hours of our report, they acknowledged the finding, successfully validated the Proof of Concept, and collaboratively pushed the patch. Their commitment to security and incredibly rapid response time sets a gold standard for open-source project management. Thanks to their swift action, secure versions (v2.1.1 and v1.10.5) were released to the public almost immediately.

The fix required moving from “String Validation” to “State-Aware Validation”. Here is the core logic of the secure validation check:

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;
}

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, then config/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 config segment and throw a security exception, stopping the fs.writeFile before 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 (which the maintainers implemented in the final patch) to prevent blocking the Node.js event loop during recursive checks.

This atomic check ensures that the logical path perfectly matches the physical filesystem layout, completely neutralizing the directory poisoning attack.

Takeaways for Developers

  1. Never trust strings for filesystem security: Functions like path.resolve() and path.normalize() are for formatting, not for security.
  2. Beware the Symlink: If your application writes files to disk based on external input, you must account for pre-existing symlinks in the destination path.
  3. Fail Gracefully: Implement robust lstat checks that halt operations immediately if the physical filesystem state diverges from your logical expectations.

Finding this bypass was a stark reminder that in cybersecurity, vulnerabilities often hide in the gap between how a developer imagines a system works and how the system actually executes.

References & Disclosure Timeline

  • Official GitHub Security Advisory: GHSA-4c3q-x735-j3r5
  • Vulnerable Package: compressing (npm)
  • Patched Versions: v2.1.1 and v1.10.5
  • CVE: CVE-2026–40931

If you found this research interesting, feel free to connect with us on

https://www.linkedin.com/in/sachin-patil-sp/

https://x.com/sachinpatilweb

https://github.com/sachinpatilpsp

https://www.linkedin.com/in/iamolofficial/

https://x.com/BuildAndDeploy

https://github.com/IAMolofficial


文章来源: https://infosecwriteups.com/uncovering-the-blind-spot-bypassing-a-security-patch-cve-2026-24884-to-achieve-arbitrary-file-bc385eaa73fa?source=rss----7b722bfd1b8d---4
如有侵权请联系:admin#unsafe.sh