What's the vulnerability?

A sandbox escape vulnerability allows sandboxed code to mutate host built-in prototypes by laundering the isGlobal protection flag through array literal intermediaries. When a global prototype reference (e.g., Map.prototype, Set.prototype) is placed into an array and retrieved, the isGlobal taint is stripped, permitting direct prototype mutation from within the sandbox. This results in persistent host-side prototype pollution and may enable RCE in applications that use polluted properties in sensitive sinks (example gadget: execSync(obj.cmd)).

Root Cause Analysis

# Root Cause Analysis: GHSA-ww7g-4gwx-m7wj

## 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:

```typescript
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:

```typescript
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

- https://github.com/nyariv/SandboxJS/commit/f369f8db26649f212a6a9a2e7a1624cb2f705b53

## Reproduction Steps

Run the reproduction script:
```bash
./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:** CVE-2026-25881
- **GHSA:** GHSA-ww7g-4gwx-m7wj
- **NVD:** https://nvd.nist.gov/vuln/detail/CVE-2026-25881
One Command

Verify with pruva-verify

Run the Pruva CLI to automatically fetch and execute the reproduction script.

pruva-verify REPRO-2026-00098
or pruva-verify GHSA-ww7g-4gwx-m7wj
or pruva-verify CVE-2026-25881
Install: curl -fsSL https://pruva.dev/install.sh | sh

Or Run Manually

1

Download the script

curl -O https://pruva.dev/api/v1/reproductions/REPRO-2026-00098/artifacts/reproduction_steps.sh
2

Make executable

chmod +x reproduction_steps.sh
3

Run the script

./reproduction_steps.sh
Run in a VM, container, or disposable environment. This exploits a real vulnerability.

How Pruva Reproduced This

Watch the AI agent's step-by-step process.

Loading session...

Artifacts

No artifacts available