XSS (DOM-based HTML injection)
Description
The fix mitigates a potential XSS in the standalone DevTools error rendering by replacing innerHTML-based HTML construction with DOM nodes and textContent. Previously, error messages were interpolated directly into an HTML string and assigned to innerHTML, which could allow HTML/JS injection if the error message came from an untrusted source. The patch now builds the error box using DOM elements and sets textContent for header and content, ensuring any embedded HTML is treated as text.
Proof of Concept
PoC steps to illustrate the risk prior to the fix:
1) In a running DevTools standalone environment, trigger an error path that calls onError with an attacker-controlled message, for example:
onError({ code: 'UNKNOWN', message: '<img src=x onerror="alert(1)">Injected' });
2) Prior to the fix, the onError code would construct a string like:
`<div class="box"><div class="box-header">Unknown error</div><div class="box-content">${message}</div></div>`
and assign it to innerHTML. The attacker-controlled content would be parsed as HTML/JS, causing potential XSS (e.g., the image tag executing onerror).
3) This would result in an injected script running in the page rendering the DevTools UI.
4) With the fix in place, the same message is placed into the DOM via textContent (not HTML), so any HTML tags are treated as text and do not execute, preventing the XSS.
Commit Details
Author: Minh Vu
Date: 2026-06-23 10:28 UTC
Message:
Avoid HTML injection in standalone errors (#36839)
## Summary
- Render standalone DevTools server errors with DOM nodes instead of
HTML strings.
- Preserve the existing error box classes and copy while inserting error
text with `textContent`.
## How did you test this?
- `corepack yarn prettier`
- `corepack yarn lint packages/react-devtools-core/src/standalone.js`
Triage Assessment
Vulnerability Type: XSS
Confidence: HIGH
Reasoning:
The commit replaces string-based HTML construction with DOM nodes and textContent for error messages, preventing HTML injection in error output. This directly mitigates potential XSS/HTML injection vulnerabilities when rendering error text.
Verification Assessment
Vulnerability Type: XSS (DOM-based HTML injection)
Confidence: HIGH
Affected Versions: 19.2.0 - 19.2.3
Code Diff
diff --git a/packages/react-devtools-core/src/standalone.js b/packages/react-devtools-core/src/standalone.js
index 3e2538d47750..32b9566bf626 100644
--- a/packages/react-devtools-core/src/standalone.js
+++ b/packages/react-devtools-core/src/standalone.js
@@ -176,31 +176,34 @@ function onDisconnected() {
disconnectedCallback();
}
+function showErrorMessage(headerText: string, contentText: string) {
+ const box = document.createElement('div');
+ box.className = 'box';
+
+ const header = document.createElement('div');
+ header.className = 'box-header';
+ header.textContent = headerText;
+ box.appendChild(header);
+
+ const content = document.createElement('div');
+ content.className = 'box-content';
+ content.textContent = contentText;
+ box.appendChild(content);
+
+ node.textContent = '';
+ node.appendChild(box);
+}
+
function onError({code, message}: $FlowFixMe) {
safeUnmount();
if (code === 'EADDRINUSE') {
- node.innerHTML = `
- <div class="box">
- <div class="box-header">
- Another instance of DevTools is running.
- </div>
- <div class="box-content">
- Only one copy of DevTools can be used at a time.
- </div>
- </div>
- `;
+ showErrorMessage(
+ 'Another instance of DevTools is running.',
+ 'Only one copy of DevTools can be used at a time.',
+ );
} else {
- node.innerHTML = `
- <div class="box">
- <div class="box-header">
- Unknown error
- </div>
- <div class="box-content">
- ${message}
- </div>
- </div>
- `;
+ showErrorMessage('Unknown error', String(message));
}
}