Feb 10, 2026·6 min read·55 visits
SandboxJS failed to persist security tags when objects were placed into array literals. Attackers could wrap a global object (like `Array.prototype`) in an array, retrieve it, and receive a 'clean' reference, allowing them to pollute the host environment.
A critical sandbox escape vulnerability in `@nyariv/sandboxjs` allows malicious code to bypass the 'isGlobal' protection flag by laundering host references through array literals. This seemingly innocuous action strips security metadata, granting attackers write access to critical host prototypes (CWE-1321) and leading to potential Remote Code Execution (RCE).
JavaScript sandboxing is like trying to hold water in a sieve. The language is so dynamic, so mutable, and so interconnected that creating a truly isolated environment is a Herculean task. @nyariv/sandboxjs attempts this by parsing and executing JS in a controlled scope, wrapping objects to track permissions. The goal is simple: let users run untrusted code without burning down the server.
However, the library relies on a concept called 'taint tracking'—specifically, an isGlobal flag attached to wrappers around sensitive host objects (like window, process, or Array.prototype). If isGlobal is true, the sandbox blocks write access. It's a 'Do Not Touch' sticker on the museum exhibits.
The problem with stickers is that they fall off if you handle the merchandise too much. In CVE-2026-25881, we found a way to peel that sticker off simply by putting the object in a box and taking it back out.
The vulnerability lies in how the sandbox handles intermediate values, specifically Array Literals. When the sandbox executes code, it wraps every object in a Prop class. This class holds the isGlobal boolean. If you try to access Array.prototype directly, the sandbox hands you a wrapper with isGlobal: true.
Here is where the logic breaks down. When you define an array literal like [Array.prototype], the sandbox's CreateArray operation extracts the raw value from the wrapper to populate the new array. It effectively says, 'Okay, you want an array containing this object? Here is the object.' It forgets to attach the 'Do Not Touch' note to the array's internal storage of that object.
When you subsequently access that index (e.g., arr[0]), the sandbox creates a new wrapper for the retrieved value. Since the retrieval didn't come from a known global lookup (it just came from some random array), the new wrapper defaults to isGlobal: false. The object has been 'laundered'. It is now a clean, writable reference to the host's actual Array.prototype.
The root cause was a philosophical error in the code: trusting the path of access rather than the identity of the object. The fix introduced in version 0.8.31 completely shifts this paradigm.
The Vulnerable Logic (Conceptual):
// Old way: Relying on metadata passing
function getProperty(obj, key) {
const value = obj[key];
// If obj was a random array, we assume 'value' is safe.
return new Prop(value, { isGlobal: false });
}The Fix (Identity Check):
The patch introduces a helper getGlobalProp that runs on every property access. It checks the value itself against a list of known dangerous globals.
// src/executor.ts
function getGlobalProp(val: unknown, context: IExecContext, prop?: Prop) {
if (!val) return;
// CRITICAL: Check if the value IS the global object
if (val === globalThis) {
return new Prop({ [p]: context.ctx.sandboxGlobal }, p, prop?.isConst, false, prop?.isVariable);
}
// Check if value is a known host function or prototype
const e = isFunc && context.evals.get(val);
if (e) {
// FORCE the protection flag back on
return new Prop({ [p]: e }, p, prop?.isConst, true, prop?.isVariable);
}
}By validating the identity of val against the context's allowlist (context.evals), the sandbox re-applies the handcuffs even if the object was laundered through a dozen arrays.
Exploiting this is embarrassingly simple. We don't need complex heap manipulation or race conditions. We just need to move an object from Point A to Point B.
The Attack Chain:
Array.prototype is perfect because it's available everywhere.Array definition for the entire host process.The PoC:
// Inside the sandbox
const laundryBasket = [Array.prototype];
const cleanShirt = laundryBasket[0]; // The 'isGlobal' flag is gone now
// Add a property to the actual host Array prototype
cleanShirt.polluted = "I am in your walls";
// Verification (in Host)
// console.log([].polluted) -> "I am in your walls"If the host application later runs code like const cmd = userOptions.cmd || 'default', and we've polluted Object.prototype to inject a malicious command, we achieve Remote Code Execution.
Why is this rated Critical (9.1)? Because SandboxJS is often used to run user-submitted scripts in SaaS platforms, rule engines, or plugin systems. Breaking out of the sandbox destroys the security model entirely.
Once an attacker has Prototype Pollution on the host, they can usually escalate to RCE. Common patterns include:
outputFunctionName in EJS).shell or env options passed to child_process.exec or spawn.toString or valueOf to throw errors or enter infinite loops.This isn't just a data leak; it's full server compromise if the host application uses any standard libraries vulnerable to pollution gadgets.
The remediation is straightforward: Update to version 0.8.31.
The patch forces the sandbox to be paranoid. It no longer trusts that a value is safe just because it came from a safe place. It checks the ID card of every object, every time it is accessed.
If you cannot update immediately, you are essentially running code on the host with extra steps. A temporary mitigation (though ugly) would be to freeze all prototypes in the host environment before starting the sandbox:
// Host-side mitigation
Object.freeze(Object.prototype);
Object.freeze(Array.prototype);
// ... repeat for all built-insThis might break other dependencies, but it stops the bleeding until you can patch.
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:H/A:H| Product | Affected Versions | Fixed Version |
|---|---|---|
@nyariv/sandboxjs nyariv | < 0.8.31 | 0.8.31 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-1321 |
| CWE Name | Prototype Pollution |
| CVSS Score | 9.1 (Critical) |
| Attack Vector | Network (AV:N) |
| Exploit Maturity | PoC Available |
| Impact | Sandbox Escape / RCE |
Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution')
A critical stored Cross-Site Scripting (XSS) vulnerability was identified in Froxlor server administration software panel before version 2.3.8. Authenticated customers with DNS editor privileges can inject malicious JavaScript into DNS TXT records. Because the application processes these values via a raw formatting callback without context-aware HTML entity encoding, the payload executes in the security context of administrative users who view the affected domain's DNS zones.
An authenticated administrator with privileges to manage admin accounts (such as change_serversettings) can execute arbitrary SQL commands via a second-order SQL injection vulnerability. The flaw resides in Froxlor's administrative API endpoints, specifically during the handling of IP address mapping parameters which are stored as serialized arrays and later interpolated without sanitization into active database queries. This vulnerability allows high-privileged administrative attackers to compromise the database. By injecting a payload into administrative profile metadata, an attacker can extract sensitive credentials, manipulate backend settings, or potentially disrupt database integrity. The vulnerability affects all versions of Froxlor prior to 2.3.8.
CVE-2026-54543 is a DNS Resource Record (RR) Injection vulnerability in Froxlor, an open-source server administration control panel. Prior to version 2.3.8, the DomainZones.add API command failed to perform strict sanitization and validation on the user-controlled record (label) and type parameters before serializing them into BIND-compatible zone files. An authenticated customer with DNS zone management permissions can inject control characters, breaking out of the original record context to define unauthorized resource records within managed zones.
CVE-2026-42533 is a critical security vulnerability discovered in NGINX Open Source, NGINX Plus, NGINX Ingress Controller, and related products, referred to as the 'Two-Pass Capture-Clobbering' bug. The flaw is situated within NGINX's internal evaluation engine when handling complex variables, exposing a heap-based buffer overflow and information leak when a configuration chains regular expression-based map directives with numbered capture groups. An unauthenticated remote attacker can exploit this weakness by transmitting crafted HTTP requests to trigger remote code execution or defeat ASLR.
Froxlor prior to version 2.3.8 contains a high-severity architectural flaw where the standalone lib/ajax.php entry point bypasses the centralized request validation in lib/init.php. Unauthenticated remote attackers can leverage Cross-Site Request Forgery (CSRF) to induce authenticated administrators to submit forged requests that modify API key whitelists and expiration dates, potentially yielding persistent, out-of-band administrative control.
An insecure data retrieval flaw in the Froxlor server administration panel API allows authenticated remote attackers to retrieve unredacted bcrypt password hashes and Base32-encoded Time-Based One-Time Password (TOTP) seeds. Affected endpoints include several 'get' and 'listing' handlers for customers, administrators, and FTP accounts. Utilizing these leaked parameters, attackers can crack the password hashes offline and concurrently generate valid second-factor authentication codes to completely bypass access controls.