# 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
