CVE-2026-40931: The compressing patch for CVE-2026-24884 can be fully bypassed — via a git-delivered symlink
7 June 2026. A vulnerability worked up publicly in early June (CVE-2026-40931, High) fully bypasses the patch for the older path-traversal flaw CVE-2026-24884 in the Node library compressing: the fix checks paths as a string only and overlooks that a directory segment on disk can be a symlink. The real twist is the delivery path: the symlink is not embedded in the archive but planted on the machine in advance via git clone, because Git restores symlinks faithfully. On later extraction fs.writeFile follows the pre-planted symlink and writes outside the target directory, up to /etc/passwd — with no interaction beyond the normal developer workflow, especially in CI/CD.
TL;DR — 90 seconds
Affected?
compressing>= 2.0.0, <= 2.1.0 and <= 1.10.4. Concretely: any Node service or CI job that uses compressing to extract an untrusted archive into a directory where a repository was previously cloned.
Risk?
Arbitrary file write outside the target directory (CWE-59, link following) → overwriting sensitive files, leading to privilege escalation and RCE (e.g. via .bashrc, startup scripts, binaries); CVSS v3 8.4 (High).
Immediate action?
Update to compressing@>=2.1.1 or >=1.10.5; audit CI pipelines that clone external repos and then extract archives.
Recommendation?
Mid-market: pin the dependency, extract in a restricted environment. Enterprise/Kubernetes: run extraction jobs unprivileged with a read-only root, and retrofit an lstat check in any custom extraction logic.
Criticality?
high (references the hero badge — a complete patch bypass with a file-write primitive; patch within the week).
What is the problem?
compressing is a widely used Node library for packing and extracting archives (tar, gzip, zip). The older flaw CVE-2026-24884 was a classic symlink path traversal: a symlink embedded in the archive could point out of the target directory during extraction, so writes landed at arbitrary locations on the filesystem. The patch for it introduced a helper, isPathWithinParent(), that checks whether the resolved target path starts with the permitted target directory.
That check is exactly the flaw. It operates purely on the string: path.resolve() in Node is a pure string manipulator that resolves .. and . arithmetically — it never looks at the disk. Whether a segment named config is a real directory or a symlink, path.resolve() does not know. If the target is /app/out and the entry resolves to /app/out/config/passwd, the startsWith check passes, even if /app/out/config is in fact a symlink to /etc. The security guard validates a logical string; the OS kernel then performs a physical write that follows the symlink. This divergence between “validated” and “executed” is the whole vulnerability.
The second, decisive part is the delivery path. The attacker need not embed anything in the archive. Instead the symlink is planted on the victim machine in advance: an attacker-controlled Git repository contains a symlink (e.g. config_file → /etc/passwd). Git treats symlinks as first-class objects and restores them faithfully on git clone. If the application then extracts a tarball with an entry named config_file, the string check passes — and the write runs through the pre-planted symlink.
Who is affected?
Affected
Not affected
Conditions / aggravating
Applications/CI using compressing>= 2.0.0, <= 2.1.0 or <= 1.10.4 that extract an untrusted archive
Applications on compressing >= 2.1.1 or >= 1.10.5
An attacker-controlled symlink with a matching name exists in (or is cloned into) the target directory beforehand
CI/CD jobs that clone external repos and then process archives in the same working directory
Pipelines that strictly separate extraction from clone directories and isolate it under an unprivileged user
Git restores the symlink automatically on git clone — no social engineering, no extra interaction required
Processes that extract as root/admin
Extraction processes with a read-only root FS and no write rights outside a scratch volume
Custom extraction logic that validates paths only via path.resolve()/startsWith
Logic that checks every path segment against disk via fs.lstatSync() (like node-tar)
The string check does not know the on-disk state
The relevant filter for my stack: compressing often appears transitively in build and tooling chains, not only as a direct dependency. An npm ls compressing across all services and CI images is therefore more telling than a look at the individual package.json.
Impact
The primitive is an arbitrary file write outside the target directory. The GitHub advisory rates the flaw CVSS v3 8.4 (High): local attack vector, low complexity, no privileges and no user interaction required, with high impact on confidentiality, integrity and availability. “Local” is misleadingly harmless here — the delivery path via git clone turns the local primitive into a supply-chain vector: anyone who clones a prepared repo and extracts an archive in the normal workflow is hit.
A file write quickly leads to more in practice. Overwriting .bashrc, .profile or startup scripts turns it into code execution at the next login or boot; overwriting binaries or configuration files yields privilege escalation. In CI/CD the situation is especially awkward, because automated jobs routinely clone foreign repos and process archives without human review — exactly the condition the exploit needs.
The real lesson lies in the patch history. CVE-2026-40931 is not a new bug type but an incomplete fix: the first correction fought the symptom (symlinks in the archive) with a string check but left the on-disk state out of scope. A “partial fix bypass” is so valuable for one’s own practice precisely because it shows that an applied patch does not equal “done” — the question is always whether the check looks at the same reality as the execution.
Mitigation / immediate steps
Note: the following steps are my operational recommendation based on the documented advisory — not a vendor-certified procedure.
Operational Decision Block
Act immediately if … a service extracts untrusted archives with compressing <= 2.1.0 / <= 1.10.4 and works in a directory where cloning also happens.
Maintenance window is fine if …compressing only processes internally produced, trusted archives — then the update path is routine, but due.
Awareness only if …compressing is not in the dependency tree (verified via npm ls).
Step 1 — Raise the version
# Update to the patched version
npm install compressing@latest
# or pin explicitly:
# >= 2.1.1 (2.x line)
# >= 1.10.5 (1.x line)
npm ls compressing # check transitive hits too
Step 2 — Decouple the CI clone/extract order
# Do NOT extract archives into the directory that was cloned into.
# Instead: a dedicated, empty scratch directory per job.
work="$(mktemp -d)"
tar -xf payload.tar -C "$work" --no-same-owner
# Run extraction unprivileged, not as root.
Step 3 — Make custom extraction logic state-aware
// Instead of a string-only check, verify every segment against disk:
const fs = require('fs');
const path = require('path');
function assertNoSymlinkInPath(childPath, parentPath) {
const dest = path.resolve(parentPath);
const target = path.resolve(childPath);
if (target !== dest && !target.startsWith(dest + path.sep)) {
throw new Error('path escapes destination');
}
let cur = dest;
for (const part of path.relative(dest, target).split(path.sep)) {
if (!part || part === '.') continue;
cur = path.join(cur, part);
try {
if (fs.lstatSync(cur).isSymbolicLink()) {
throw new Error(`symlink segment detected: ${cur}`);
}
} catch (err) {
if (err.code === 'ENOENT') break; // path doesn't exist yet -> ok
throw err;
}
}
}
Detection / verification
Inventory — where is compressing?
# Across all repos/images: find affected versions
grep -REl '"compressing"' --include=package-lock.json .
# per hit, check the resolved version:
npm ls compressing
Runtime — writes outside the scratch volume
Falco rule (example) that flags writes by extraction processes outside the permitted path:
- rule: compressing extraction writes outside scratch
desc: Node process writes outside the permitted extraction directory
condition: >
open_write and proc.name=node
and not fd.name startswith /scratch/
and (fd.name startswith /etc/ or fd.name startswith /root/ or fd.name startswith /home/)
output: "Write outside scratch (file=%fd.name proc=%proc.cmdline)"
priority: WARNING
Tetragon alternative: a TracingPolicy on security_inode_link/security_path_symlink in the build namespace that logs symlink creation with targets outside the working directory. For a quick check it often suffices to run a test clone of a known PoC repo in a sandbox and observe whether the patched version aborts extraction with a security exception.
Operator guidance
The line is the same across all operating models: patching is necessary, but the structural lesson is separating clone and extraction directories plus unprivileged extraction.
Mittelstand
Pin compressing to >= 2.1.1 / >= 1.10.5 and enforce a dedicated scratch directory per job in CI. Anyone who clones foreign repos and then processes archives should keep the two spatially separate — it costs little and removes the basis for the vector.
Enterprise
Use the SBOM to capture every service with compressing in the tree (including transitively), roll out the patch via the release pipeline, and run extraction jobs unprivileged with a read-only root and a single writable scratch mount.
Kubernetes
Run extraction workloads with readOnlyRootFilesystem: true, runAsNonRoot: true and an emptyDir scratch; the file write then runs into the void, even if an unpatched version was missed. A Falco/Tetragon rule on writes outside the scratch mount adds defense-in-depth.
Declarative stacks (NixOS/Talos/Flatcar)
Anchor the version pin declaratively and harden build runners so extraction steps have no write rights outside their work area. The immutable host removes the impact of overwriting system files anyway.
What I actually did
I inventoried my Node and tooling images against compressing, moved the few transitive hits onto the patched line, and separated the clone from the extraction directories in the affected pipelines. Extraction runs unprivileged for me anyway, with a read-only root and a single scratch mount; that layer would have caught the file write even with a missed version — which is the second lesson of this case.
The first lesson is about patches: CVE-2026-40931 is proof that “patch applied” does not mean “problem solved” when the check thinks in a different model than the execution. The first fix checked strings, but the filesystem executes physical paths. Anyone running their own extraction, upload or path-validation logic should make it state-aware: check every segment against disk via lstat, as node-tar demonstrates — it is not the string that decides, but what actually sits on disk. And the git-clone delivery path is the uncomfortable punchline against the worm wave of recent days: there the rule was “cloning is safe, opening is not”; here cloning is the delivery path itself, because Git brings the symlink along faithfully.
Frequently asked questions about CVE-2026-40931 (compressing)
Is npm install --ignore-scripts enough against CVE-2026-40931?+
No. This flaw needs no install hook — it is triggered when the application itself extracts an archive. --ignore-scripts addresses a different vector. Here only the version update (>= 2.1.1 / >= 1.10.5) plus separating clone and extraction directories helps.
How do I check whether compressing is transitively in my stack?+
npm ls compressing across every project and every CI image; a hit in a transitive dependency counts just like a direct one. Additionally grep across all package-lock.json files and compare each resolved version against <= 2.1.0 / <= 1.10.4.
Are pure runtime containers without git affected?+
The git-clone delivery path is absent there, but a symlink already present in the image or introduced via a mounted volume can have the same effect. Patching remains correct, and archives belong in a dedicated, empty scratch directory.
Do Kubernetes pods that only extract internally produced archives need to act immediately?+
The risk is lower because both the archive and the target directory are trusted. Still, pull the version pin in the next maintenance window — the effort is minimal. Anyone already running with readOnlyRootFilesystem: true and an emptyDir scratch has already defused the impact.
Why is this rated High when the attack vector is Local?+
Because the git-clone delivery path turns the local primitive into a supply-chain vector: a prepared repo plus the normal developer/CI workflow suffice, with no extra privileges or interaction. The GitHub advisory rates the flaw CVSS v3 8.4 accordingly.
What distinguishes this flaw from the Miasma/IronWorm wave?+
This is not a self-replicating worm and not install-hook abuse, but a library patch bypass with a file-write primitive. The only common denominator is the supply-chain character via git/CI — and the punchline: in the worm wave the rule was “cloning is safe, opening is not”; here cloning is the delivery path itself.
Conclusion
CVE-2026-40931 is not a spectacular new attack class but an instructive one: a security patch that fights the right danger with the wrong method. The string check looked safe but did not know the on-disk state, and the attacker delivers the decisive on-disk state conveniently via git clone. The operational severity is real but well manageable: raise the version, separate clone and extraction directories, extract unprivileged. The most important recommendation is the structural one — make your own path validation state-aware and measure patches by whether the check looks at the same reality as the execution. Neither dramatize nor downplay: a clear patch case with a point that carries beyond this one bug.
Before the next prepared repo lands in your CI — let’s talk about your extraction path.
I assess, mitigate and validate your Node and CI pipelines against symlink and supply-chain vectors.
SBOM inventory across all services and images, patch rollout in the maintenance window, separation of clone and extraction directories, unprivileged extraction with a read-only root — and a PoC validation that proves the patched version actually aborts.
This is platform operations, not advice on paper: I harden your CI/CD path against exactly these “patch applied but not done” cases.
Programming since 2002 – self-taught, set up my own business with KO-Web in 2012. Over 100 projects, with a focus on security, performance, automation and quality. Today freelance: DevSecOps consulting, training and software development.