CVE-2026-117977
Overview
Background
- Web Console (DevTools)
devtools/client/webconsole/actions/messages.jsimplements console UI actions.messageGetMatchingElementspowers the “matching elements” feature: given a console message that carries CSS selectors, it asks the inspected page how many/which elements those selectors match.- `commands.scriptCommand.execute(expr, ...)`
- evaluates a JavaScript expression string
exprin the context of the inspected target (the page/frame identified byselectedTargetFront/innerWindowID). - `message.cssSelectors`
- a selector string originating from a console message, i.e. content/page-influenced data.
- `CSS.escape(value)`
- standard DOM API that escapes a string so it is safe to embed as a CSS identifier/selector literal, neutralizing characters like quotes and backslashes.
Root Cause Analysis
messageGetMatchingElements built the evaluated expression by directly string-interpolating the page-derived selector into a JavaScript source string:
document.querySelectorAll('${message.cssSelectors}'). That whole string is then handed to commands.scriptCommand.execute(...) and evaluated as JavaScript in the target. Because message.cssSelectors is attacker-influenced and is placed inside a single-quoted JS string literal with no escaping, a crafted selector containing a single quote (and arbitrary following text) breaks out of the string literal and injects attacker-chosen JavaScript into the evaluated expression. The violated invariant is that untrusted data embedded into an evaluated code string must be escaped/encoded for that context; it was not.
The patch wraps the value: document.querySelectorAll('${CSS.escape(message.cssSelectors)}'). CSS.escape encodes the selector so embedded quotes/backslashes can no longer terminate the string literal, preventing the breakout and confining the value to a harmless selector argument. (The advisory’s generic “Other issue in the Console component” understates this: the diff substantiates a code-injection/input-validation flaw in an evaluated expression.)
message.cssSelectors escapes the literal and injects script. CSS.escape() neutralizes the breakout.Attack Path
- Emit a crafted message
A malicious page produces a console message whose
cssSelectorsfield contains a payload like') ; <arbitrary JS> //designed to close the string literal. (web-reachable: the page controls console message content.) - User invokes the feature
The developer opens the Web Console and triggers “get matching elements” on that message, calling
messageGetMatchingElements. - Injection into evaluated code
The unescaped selector is interpolated into
document.querySelectorAll('...'); the quote terminates the literal and the attacker’s JavaScript becomes part of the expression string. - Execution in target
scriptCommand.executeevaluates the combined string in the inspected target context, running attacker JavaScript there. - Primitive Arbitrary JavaScript execution in the inspected target’s context, driven by page-supplied data rather than the developer’s input.
Impact Assessment
scriptCommand.execute. Exploitation requires the victim to open the Web Console and use the matching-elements action on an attacker-influenced message (user interaction gating). The injected code runs in the inspected target’s execution context; because that context is the page the attacker already influences, the direct gain is bounded, but evaluating attacker-chosen expressions through the DevTools evaluation path can broaden beyond what the page’s own script could do and is the reason it is tracked as a security fix. The fix is a straightforward escaping of untrusted input; no memory-safety sink is involved.Files Changed
devtools/client/webconsole/actions/messages.js
Audit Directions
- String-built evaluated expressionsgrep DevTools for template literals passed to
scriptCommand.execute/eval-style evaluators that interpolatemessage.*, node/attribute values, or other target-derived data without escaping. - Selector/identifier interpolationlook for other places user/page strings are embedded in
querySelector,querySelectorAll, or generated CSS/JS, and verifyCSS.escape/JSON.stringifyis applied. - Console message fields as trust boundaryreview all consumers of
message.*fields in the webconsole actions/reducers treating them as untrusted, since they derive from page output.
Patch
diff --git a/devtools/client/webconsole/actions/messages.js b/devtools/client/webconsole/actions/messages.js
index ac14fa4e49d..e8d98d70cf4 100644
--- a/devtools/client/webconsole/actions/messages.js
+++ b/devtools/client/webconsole/actions/messages.js
@@ -123,7 +123,7 @@ function messageGetMatchingElements(message) {
const selectedTargetFront = message?.targetFront;
const response = await commands.scriptCommand.execute(
- `document.querySelectorAll('${message.cssSelectors}')`,
+ `document.querySelectorAll('${CSS.escape(message.cssSelectors)}')`,
{
selectedTargetFront,
innerWindowID: message.innerWindowID,