CVE-2026-25881: Verified Repro With Script Download
CVE-2026-25881: SandboxJS: Host Prototype Pollution via Array Intermediary Sandbox Escape
CVE-2026-25881 is verified against @nyariv/sandboxjs · npm. Affected versions: <= 0.8.30. Fixed in 0.8.31. Vulnerability class: RCE. This critical reproduction includes runnable sandbox proof, artifacts, and a plain-text agent view under REPRO-2026-00098.
What Is CVE-2026-25881?
CVE-2026-25881 is a critical sandbox-escape vulnerability in @nyariv/sandboxjs where sandboxed code can launder the isGlobal protection flag through an array-literal intermediary to mutate host built-in prototypes such as Map.prototype and Set.prototype, potentially enabling remote code execution in applications that use the polluted properties in sensitive sinks. Pruva reproduced it (reproduction REPRO-2026-00098).
CVE-2026-25881 Severity & CVSS Score
CVE-2026-25881 is rated critical severity, with a CVSS base score of 9.0 out of 10.
Critical — the most severe class — typically remotely exploitable with severe impact. Treat as an emergency.
Affected @nyariv/sandboxjs Versions
@nyariv/sandboxjs · npm versions <= 0.8.30 are affected.
How to Reproduce CVE-2026-25881
pruva-verify REPRO-2026-00098 curl -O https://pruva.dev/api/v1/reproductions/REPRO-2026-00098/artifacts/reproduction_steps.sh && chmod +x reproduction_steps.sh && ./reproduction_steps.sh Proof of Reproduction for CVE-2026-25881
Reproduced by Pruva's autonomous agents — 116 tool calls over 16 min. Full root-cause analysis and the complete transcript are below.
How the agent worked
Root Cause and Exploit Chain for CVE-2026-25881
Summary
A sandbox escape vulnerability in @nyariv/sandboxjs (versions <= 0.8.30) allows sandboxed untrusted code to bypass the isGlobal protection mechanism and pollute host built-in prototypes (e.g., Map.prototype, Set.prototype). The vulnerability occurs when global prototype references are placed into array literals - the isGlobal taint flag is stripped during array creation via valueOrProp(), allowing the retrieved prototype to be modified directly from within the sandbox.
Impact
- Package:
@nyariv/sandboxjs(npm) - Affected Versions:
<= 0.8.30 - Fixed In:
0.8.31 - CVSS Score: 9.1 (Critical)
- CWE: CWE-1321 (Improperly Controlled Modification of Object Prototype Attributes)
Risk: This is a sandbox escape vulnerability that breaks isolation between untrusted sandboxed code and the host environment. Attackers can:
- Persistently pollute host prototypes (e.g.,
Map.prototype,Set.prototype) - Overwrite built-in methods (e.g.,
Set.prototype.has) - Inject properties that can be used in RCE gadgets if host code uses polluted properties in sensitive sinks like
child_process.execSync()
Consequences: Any application using this package to execute untrusted JavaScript is vulnerable to complete host compromise.
Root Cause
Technical Explanation
The sandbox uses a Prop class with an isGlobal flag to track whether a value is a global/prototype that should be protected from modification. The set() method in src/utils.ts checks this flag:
set(key: string, val: unknown) {
// ...
if (prop.isGlobal) {
throw new SandboxError(`Cannot override global variable '${key}'`);
}
(prop.context as any)[prop.prop] = val;
return prop;
}
However, when values pass through array literal creation in src/executor.ts (lines 559-571), the valueOrProp() function unwraps Prop objects into raw values, stripping the isGlobal taint:
addOps(LispType.CreateArray, (exec, done, ticks, a, b: Lisp[], obj, context, scope) => {
const items = (b as LispItem[])
.map((item) => {
if (item instanceof SpreadArray) {
return [...item.item];
} else {
return item;
}
})
.flat()
.map((item) => valueOrProp(item, context)); // <- isGlobal flag lost here
done(undefined, items);
});
When Map.prototype is accessed via [Map.prototype][0], the retrieved value no longer has the isGlobal flag, bypassing the protection.
Fix Commit
Reproduction Steps
Run the reproduction script:
./repro/reproduction_steps.sh
The script performs three tests:
Prototype Pollution Test: Places
Map.prototypeinto an array, retrieves it, and setspolluted='pwned'. Verifies that'polluted' in Map.prototypereturnstrueandMap.prototype.polluted === 'pwned'.Method Overwrite Test: Retrieves
Set.prototypevia array and overwritesSet.prototype.haswithisFinite. Verifies the overwrite succeeds.RCE Gadget Test: Pollutes
Map.prototypewithcmd='id', demonstrating thatnew Map().cmdreturns'id', which could be passed toexecSync()by vulnerable host code.
Expected Evidence
The script outputs [PASS] for all three tests and confirms:
"polluted" in Map.prototype = trueMap.prototype.polluted = pwnedSet.prototype.has === isFinite: truenew Map().cmd = id
Evidence
Log Files:
logs/reproduction.log- Complete reproduction output
Key Excerpts from Successful Run:
[TEST 1] Prototype pollution via array intermediary
Before: "polluted" in Map.prototype = false
After: "polluted" in Map.prototype = true
Map.prototype.polluted = pwned
[PASS] Prototype pollution confirmed!
[TEST 2] Overwrite Set.prototype.has
Set.prototype.has === isFinite: true
[PASS] Set.prototype.has was successfully overwritten!
[TEST 3] RCE gadget via prototype pollution
new Map().cmd = id
[PASS] RCE gadget works - injected command in prototype!
Environment:
- Node.js (tested with npm-installed package)
@nyariv/sandboxjsversion 0.8.30 (vulnerable)
Recommendations / Next Steps
Immediate Actions
- Upgrade to version 0.8.31 or later - The fix commit addresses the taint stripping issue
- Audit existing deployments - Check if any applications using this package process untrusted user input
Fix Approach (for maintainers)
- Preserve
isGlobalthrough array/object literals: Modify the array/object creation code to preserve thePropwrapper instead of unwrapping viavalueOrProp() - Hard block built-in prototype writes: Add explicit checks for built-in prototype objects (
Map.prototype,Set.prototype, etc.) regardless of how they're obtained - Defense in depth: Consider freezing built-in prototypes before running untrusted code
Testing Recommendations
Add regression tests that:
- Attempt prototype pollution via
[Map.prototype][0],[Set.prototype][0], etc. - Verify the
isGlobalprotection is maintained through array/object literals - Test method overwriting attempts on built-in prototypes
Additional Notes
Idempotency Confirmation
The reproduction script has been verified to pass two consecutive runs:
- Run 1: All tests passed
- Run 2: All tests passed
The script includes cleanup to remove prototype pollution after testing (delete Map.prototype.polluted, etc.), ensuring idempotent behavior.
Edge Cases
- Other built-in prototypes (
Array.prototype,Object.prototype,Promise.prototype, etc.) may also be vulnerable via the same vector - Object literals (not just arrays) may have similar taint-stripping issues
- The vulnerability requires the sandbox to have access to global prototypes, which is the default configuration
Related Identifiers
- CVE: CVE-2026-25881
- GHSA: GHSA-ww7g-4gwx-m7wj
- NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-25881
CVE-2026-25881 Reproduction Transcript
The agent's step-by-step process — every tool call, every handoff, the moment the exploit fired.
Full session Replay every step — scrub the timeline or play it back.
cd /workspace && curl -s https://github.com/nyariv/SandboxJS/commit/f369f8db26649f212a6a9a2e7a1624cb2f705b53.diff | head -200{"cwd":"/root/.pruva/runs/ghsa-ww7g-4gwx-m7wj_20260219-193042","duration_ms":380,"exit_code":0,"stderr":"","stdout":"diff --git a/eslint.config.js b/eslint.config.js\nindex 20934fe..7e123e6 100644\n--- a/eslint.config.js\n+++ b/eslint.config.js\n@@ -46,7 +46,7 @@ module.exports = [\n '@typescript-eslint/no-unused-expressions': 'warn',\n '@typescript-eslint/no-unsafe-function-type': 'off',\n 'no-fallthrough': 'off',\n- '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^lastLastLastLastPart' }],\n+ '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^lastLastLastLastPart' }],\n '@typescript-eslint/no-unsafe-assignment': 'off',\n }\n }\ndiff --git a/package-lock.json b/package-lock.json\nindex ee9223e..c00918c 100644\n--- a/package-lock.json\n+++ b/package-lock.json\n@@ -1,12 +1,12 @@\n {\n \"name\": \"@nyariv/sandboxjs\",\n- \"version\": \"0.8.28\",\n+ \"version\": \"0.8.31\",\n \"lockfileVersion\": 3,\n \"requires\": true,\n \"packages\": {\n \"\": {\n \"name\": \"@nyariv/sandboxjs\",\n- \"version\": \"0.8.28\",\n+ \"version\": \"0.8.31\",\n \"license\": \"MIT\",\n \"… [truncated]Artifacts and Evidence for CVE-2026-25881
Scripts, logs, diffs, and output captured during the reproduction.
No artifacts available
How to Fix CVE-2026-25881
Upgrade @nyariv/sandboxjs · npm to 0.8.31 or later.
FAQ: CVE-2026-25881
How does the SandboxJS array-laundering prototype-pollution sandbox escape work?
Which versions of @nyariv/sandboxjs are affected by CVE-2026-25881, and where is it fixed?
How severe is CVE-2026-25881?
How can I reproduce CVE-2026-25881?
References for CVE-2026-25881
Authoritative sources for CVE-2026-25881 — official vulnerability databases and the upstream advisory. Pruva's reproduction verifies the issue firsthand; these are the primary records to corroborate it.