Skip to content

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.

REPRO-2026-00098 @nyariv/sandboxjs · npm RCE Feb 19, 2026 CVE entry ↗ .txt
Severity
CRITICAL
CVSS
9.0
Reproduced in
16m 1s
Tool calls
116
Spend
$0.42
01 · Overview

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).

02 · Severity & CVSS

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 threat level
9.0 / 10 CVSS base
Weakness CWE-1321 — Improperly Controlled Modification of Object-Attribute Properties and Methods

Critical — the most severe class — typically remotely exploitable with severe impact. Treat as an emergency.

03 · Affected Versions

Affected @nyariv/sandboxjs Versions

@nyariv/sandboxjs · npm versions <= 0.8.30 are affected.

How to Reproduce CVE-2026-25881

$ pruva-verify REPRO-2026-00098
or curl -O https://pruva.dev/api/v1/reproductions/REPRO-2026-00098/artifacts/reproduction_steps.sh && chmod +x reproduction_steps.sh && ./reproduction_steps.sh
Run in a VM or disposable container. This exploits a real vulnerability.
06 · Proof of Reproduction

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 402 events · 116 tool calls · 16 min
16 minDuration
116Tool calls
93Reasoning steps
402Events
2Dead-ends
Agent activity over 16 min
Support
17
Repro
244
Variant
136
0:0016:01

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:

  1. Persistently pollute host prototypes (e.g., Map.prototype, Set.prototype)
  2. Overwrite built-in methods (e.g., Set.prototype.has)
  3. 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:

  1. Prototype Pollution Test: Places Map.prototype into an array, retrieves it, and sets polluted='pwned'. Verifies that 'polluted' in Map.prototype returns true and Map.prototype.polluted === 'pwned'.

  2. Method Overwrite Test: Retrieves Set.prototype via array and overwrites Set.prototype.has with isFinite. Verifies the overwrite succeeds.

  3. RCE Gadget Test: Pollutes Map.prototype with cmd='id', demonstrating that new Map().cmd returns 'id', which could be passed to execSync() by vulnerable host code.

Expected Evidence

The script outputs [PASS] for all three tests and confirms:

  • "polluted" in Map.prototype = true
  • Map.prototype.polluted = pwned
  • Set.prototype.has === isFinite: true
  • new 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/sandboxjs version 0.8.30 (vulnerable)

Recommendations / Next Steps

Immediate Actions
  1. Upgrade to version 0.8.31 or later - The fix commit addresses the taint stripping issue
  2. Audit existing deployments - Check if any applications using this package process untrusted user input
Fix Approach (for maintainers)
  1. Preserve isGlobal through array/object literals: Modify the array/object creation code to preserve the Prop wrapper instead of unwrapping via valueOrProp()
  2. 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
  3. 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 isGlobal protection 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-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.

Event 1/40
0:001:40
0:00
session startedaccounts/fireworks/models/kimi-k2p5 · ghsa-ww7g-4gwx-m7wj · ghsa-ww7
0:02
0:16
0:19
web search
0:24
0:28
web search
0:37
0:37
extract_facts
no facts extracted
0:43
0:43
0:43
supportrepro
1:21
1:21
1:21
1:25
1:25
1:25
1:30
1:33
web search
1:34
1:34
$cd /workspace && curl -s https://github.com/nyariv/SandboxJS/commit/f369f8db26649f212a6a9a2e7a1624cb2f705b53.diff | head -200
0.4s
{"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]
1:40

Artifacts and Evidence for CVE-2026-25881

Scripts, logs, diffs, and output captured during the reproduction.

No artifacts available

08 · How to Fix

How to Fix CVE-2026-25881

Upgrade @nyariv/sandboxjs · npm to 0.8.31 or later.

Coming soon

Step-by-step mitigation and hardening guidance for CVE-2026-25881 — configuration checks, workarounds where no patch exists, and how to verify you're protected — is on the way.

10 · FAQ

FAQ: CVE-2026-25881

How does the SandboxJS array-laundering prototype-pollution sandbox escape work?

Sandboxed code places a reference to a global prototype (e.g. Map.prototype or Set.prototype) into an array literal, then retrieves it back out of that array. Because valueOrProp() strips the isGlobal taint during this round-trip, the sandbox's set() guard no longer blocks direct mutation of that prototype, letting the sandboxed code permanently overwrite built-in methods (e.g. Set.prototype.has) on the host -- and inject properties that can drive RCE if host code later uses them in sinks like execSync(obj.cmd).

Which versions of @nyariv/sandboxjs are affected by CVE-2026-25881, and where is it fixed?

Versions <= 0.8.30 are affected. It is fixed in 0.8.31.

How severe is CVE-2026-25881?

Critical (CVSS 9.1, CWE-1321) -- it breaks the isolation boundary between untrusted sandboxed code and the host, allowing persistent host prototype pollution and, depending on host code, potential remote code execution.

How can I reproduce CVE-2026-25881?

Download the verified script from this page and run it in an isolated environment against @nyariv/sandboxjs <= 0.8.30. It runs sandboxed code that places Map.prototype (or Set.prototype) into an array, retrieves it back out, and mutates it directly, then shows the mutation is visible on the host's global prototype outside the sandbox.
11 · References

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.