Feb 28, 2026·5 min read·67 visits
Logic error in malcontent < 1.21.0 deletes nested archives even if extraction fails, allowing malformed malicious files to bypass scanning.
A logic vulnerability in the `malcontent` supply chain security tool allows attackers to bypass detection engines. The issue resides in the handling of nested archives (e.g., a `.tar` inside a `.zip`). When the scanner attempts to extract a nested archive and fails due to malformed headers or corruption, it unconditionally deletes the source archive file. This removal occurs before the content scanning phase, effectively hiding the malicious payload from analysis. Attackers can exploit this by crafting archives that fail extraction but contain malicious signatures in their raw bytes.
malcontent is an open-source security tool developed by Chainguard to detect supply-chain compromises. It employs a combination of context analysis, differential analysis, and YARA signatures to identify malicious code within software artifacts. A core feature of the tool is its ability to recursively unpack nested archives to inspect their contents.
The vulnerability, identified as CVE-2026-28407, affects the extraction logic for these nested archives. In affected versions (< 1.21.0), the scanner enforces a cleanup policy that is too aggressive. When malcontent encounters a nested archive, it attempts to extract it to a temporary directory for analysis. Crucially, the code was designed to remove the temporary archive file after the extraction attempt to save space. However, this removal was performed unconditionally, regardless of whether the extraction succeeded or failed.
This behavior creates a race-condition-like logic flaw where the artifact containing potential malware is deleted from the disk specifically because it failed to unpack. Since the subsequent scanning phases (YARA and heuristics) operate on the files remaining on the disk, the malicious artifact simply vanishes from the scanner's view, resulting in a false negative report.
The root cause is an improper handling of exceptional conditions (CWE-703) within the extractNestedArchive function in pkg/archive/archive.go (and related extraction logic). The function is responsible for orchestrating the decompression of files found within other files.
The logic flow in vulnerable versions was as follows:
tar, zip, gzip).os.Remove(archivePath) to delete the nested archive file.By deleting the file even after a failure, the code assumes that if extraction failed, the file is either useless or benign garbage. This assumption is incorrect in a security context. A file can be a valid executable or contain a valid YARA signature while simultaneously having a corrupted archive header that causes standard extraction libraries to error out. The scanner relies on inspecting the extracted content OR the raw content. By deleting the raw content after failing to produce extracted content, the scanner inspects nothing.
The vulnerability is evident in the patch diff provided by the maintainers. The fix involves wrapping the deletion logic in a conditional block that checks if the extraction error was nil.
In the original code, the cleanup os.Remove runs regardless of the err state from the extraction function.
// ... inside the extraction loop ...
err := extract(f, d)
if err != nil {
// Error is logged, but control flow continues
logger.Debugf("ignoring extraction error for %s: %s", f, err.Error())
}
// CRITICAL FLAW: The source file 'fullPath' is deleted even if extraction failed.
// This removes the evidence before the scanner can look at it.
if err := os.Remove(fullPath); err != nil {
return fmt.Errorf("failed to remove archive file: %w", err)
}The fix introduced in version 1.21.0 explicitly preserves the file if extraction fails. This allows the scanner to fall back to scanning the raw file itself.
// ... inside the extraction loop ...
err := extract(f, d)
// ... error logging ...
// FIX: Only attempt to remove the archive file if we successfully extracted it.
// If 'err' is not nil, the file remains on disk for the scanner to inspect.
if err == nil {
if err := os.Remove(fullPath); err != nil {
return fmt.Errorf("failed to remove archive file: %w", err)
}
}Exploiting this vulnerability requires an attacker to craft a "broken" archive that is technically invalid enough to fail the specific Go extraction library used by malcontent, yet still contains the malicious payload in its byte stream.
payload.bin is put inside inner.tar.gz.inner.tar.gz or truncates the footer. The goal is to ensure that gzip or tar readers return an error (e.g., unexpected EOF or invalid header) immediately upon processing, or partially through processing.inner.tar.gz is packed inside a valid outer.zip.When malcontent scans outer.zip, it successfully extracts inner.tar.gz. It then identifies inner.tar.gz as an archive and attempts to extract it. The extraction fails due to the corruption. The vulnerable code catches the error, logs "ignoring extraction error", and immediately deletes inner.tar.gz.
The scanner then iterates over the directory. Since inner.tar.gz was deleted and no files were extracted from it, the directory appears empty or benign. The malicious payload, which existed in the raw bytes of inner.tar.gz, is gone.
The primary impact of CVE-2026-28407 is a Security Control Bypass. malcontent is relied upon to gate software supply chains, often running in CI/CD pipelines to block malicious dependencies or artifacts.
By successfully bypassing this check, an attacker can introduce malware into a trusted environment. While the vulnerability does not directly grant remote code execution (RCE) on the scanning infrastructure itself, it neutralizes the scanner's ability to protect downstream systems.
Severity Metrics:
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:L/VA:N/SC:N/SI:N/SA:N. The attack vector is Network, and complexity is Low. The impact is framed as Low Integrity impact because the integrity of the scan results is compromised.malcontent version < 1.21.0 for artifact validation.CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:L/VA:N/SC:N/SI:N/SA:N| Product | Affected Versions | Fixed Version |
|---|---|---|
malcontent chainguard-dev | < 1.21.0 | 1.21.0 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-703 |
| CVSS v4.0 | 6.9 (Medium) |
| Attack Vector | Network |
| Vendor | Chainguard |
| Exploit Status | POC Available |
| Patch Status | Released (v1.21.0) |
Improper Check or Handling of Exceptional Conditions
A module allowlist bypass vulnerability (CVE-2026-92945 / GHSA-7q3f-wx44-378m) was identified in the vm2 sandboxing library prior to version 3.11.7. This flaw permits unauthenticated or untrusted code running within the sandbox environment to bypass explicit module restrictions. When the transitive resolution option is disabled, the system fails to validate file system path boundaries, allowing prefix-sharing sibling directories to be resolved and loaded, thereby escaping intended sandbox restrictions.
CVE-2026-92941 is a critical sandbox-escape and trust-manipulation vulnerability in the vm2 library (versions 3.11.3 to 3.11.6). This security flaw allows untrusted code executing within a NodeVM sandbox environment to compromise the global TLS trust store of the host Node.js process. By leveraging a design flaw where the host's native 'tls.setDefaultCACertificates' can be executed via a proxy wrapper, combined with a bridge unwrapping bypass in the 'url' module, an attacker can modify the process-wide default root Certificate Authorities. Consequently, all subsequent outbound TLS/HTTPS clients running on the host thread are forced to trust attacker-signed certificates, facilitating transparent Man-in-the-Middle (MitM) attacks. The vulnerability was resolved in version 3.11.7 of vm2 by introducing built-in member-level sanitization before applying read-only proxy wrappers.
A critical engine-level reachability failure in Node.js 26 running V8 14.6 allows attackers to escape the vm2 sandbox environment. When consecutive prototype properties are modified using sequential assignments, a V8 optimization bug fails to invalidate the PromiseThenLookupChain protector. By calling Promise.prototype.finally, the attacker bypasses the vm2 wrappers, hijacks the promise reaction using a custom constructor, triggers a calibrated stack overflow to capture a host-realm RangeError, and executes arbitrary shell commands on the host.
A critical sandbox escape vulnerability in the vm2 library allows sandboxed JavaScript code to bypass containment and execute arbitrary native code on the host process. This occurs when the host's builtin crypto module is exposed to the NodeVM environment. Although vm2 implements a read-only proxy layer to restrict direct modifications to host properties, it does not prevent invocation of host-level functions. By calling the crypto.setEngine API with a path to a malicious native library on disk, an attacker can trigger OpenSSL's dynamic module loader. The host's operating system loader immediately runs the dynamic library's initializers/constructors before verifying engine compatibility, leading to remote code execution in the context of the host process.
CVE-2026-92938 is a critical sandbox escape vulnerability in the vm2 library (versions 3.11.3 through 3.11.6) that allows arbitrary native code execution on the host when the node:sqlite built-in module is loaded inside a sandboxed NodeVM environment.
CVE-2026-92937 is a critical sandbox escape vulnerability in the `vm2` Node.js library. Due to a logical failure in checking direct invocation targets inside the Proxy bridge, an attacker can register Promise callbacks using `Function.prototype.call` or `Function.prototype.apply` indirection. This bypasses the error sanitization wrappers, delivering raw host error objects directly to sandboxed callbacks and allowing the attacker to escape the sandbox and execute arbitrary shell commands on the host.