
Chrome DevTools XPath safety – escape the JavaScript literal not the XPath
Prevent RCE when you inject user-XPath into inspectedWindow.eval by escaping the JavaScript literal, not the XPath itself.
XPath, or XML Path Language, is a query language designed to navigate through elements and attributes in an XML document. The document.evaluate() method in JavaScript allows developers to execute XPath expressions against an XML document, returning nodes or values based on the specified query. While this functionality is powerful, it also poses risks of injection attacks if user input is not properly handled.
When a simple
XPath field becomes Remote Code Execution (RCE)
You ship a DevTools panel that lets developers paste an XPath to highlight an element.
One user types:
"]}); document.documentElement.innerHTML='<iframe src="javascript:alert(document.cookie)"></iframe>';({Your code happily concatenates that string into inspectedWindow.eval and arbitrary JavaScript runs in the inspected page. The Chrome Web Store rejection e-mail lands minutes later.
By the end of this article you’ll have a few lines encoder that passes ESLint, keeps JSON.parse happy, and satisfies the strictest security audit.
Why document.evaluate itself is innocent
XPath 1.0 has no eval(), no inline scripts, no entity expansion tricks. The injection risk appears before the XPath engine ever sees the text-while the JavaScript parser is still tokenising the literal that contains your expression.
The JavaScript meta-character trap
These bytes let an attacker break out of a JavaScript string literal:
| Code points | Character | Why dangerous |
|---|---|---|
U+0000 – U+001F | control codes | eslint screams; some are illegal in JSON |
U+0022 | " | terminates the literal |
U+005C | \ | starts an escape sequence |
JSON.stringify is not enough as it produces JSON, not a JavaScript literal. A lone U+2028 or U+2029 is valid JSON but still breaks old JavaScript parsers, and it does nothing for control codes below U+0020.
// ❌ NEVER DO THIS
chrome.devtools.inspectedWindow.eval(
`document.evaluate("${xpath}", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null)`
);Sanitizing XPath
A safe encoder in a few lines:
function sanitizeXPath(str) {
return JSON.stringify(str)
.replace(/\u2028|\u2029/g, (ch) => {
return `\\u${ch.charCodeAt(0).toString(16).padStart(4, '0')}`;
})
}- The
U+2028/U+2029are valid in JSON but are line separators in JavaScript source (and historically break older parsers). The extra two escapes remove that footgun. - Four-digit
\uXXXXkeeps JSON.parse / ESLint happy. - No dependencies, tree-shake friendly.
Usage:
const literal = sanitizeXPath(userXPath);
const expr = `document.evaluate(${literal}, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null)`;
chrome.devtools.inspectedWindow.eval(expr, resultCallback);Integration recipes
inspectedWindow.evalconst safe = sanitizeXPath(userXPath);
chrome.devtools.inspectedWindow.eval(safe, resultCallback);const fn = new Function('document',
`return document.evaluate("${sanitizeXPath(xpath)}",document,null,9,null).singleNodeValue`
);const xpath = JSON.parse("{{ xpath_escaped|json_encode }}"); // \uXXXX alreadySolutions
that fail security review
- Regex whitelist
[a-zA-Z0-9/_\[\]]– breaks valid predicates, still allows injection via]. - Back-tick template without tagging – newline injection,
`$`interpolation. We only support Chrome 120+ so we can use eval safely
– no, you can’t.
Short security checklist
- Escape
",\,U+0000-U+001F. - Escape
U+2028andU+2029. - Do not place user input into template literals without tagging / escaping.
- Use
JSON.stringify+ replace or thesanitizeXPathroutine, include unit tests. - Continuous integration (CI) test that feeds malicious payloads and asserts no arbitrary JavaScript execution.
- Document threat model and note XPath-level DoS risk separately.
- Keep integration examples consistent (always embed the quoted literal produced by your function and don’t add extra quotes).
Final words
XPath is a powerful ally for hunting nodes in the DOM, but the moment you ferry a user-supplied expression through a JavaScript string you have left the cosy world of queries and entered the mine-field of parsers.
A single unescaped quote or back-slash is all it takes to turn document.evaluate into eval and grant an attacker the same privileges your extension enjoys.
A small encoder in this article closes that door forever: it speaks JSON, keeps ESLint quiet, and survives the strictest security audit Chrome can throw at you. Copy it, add the unit tests to your CI pipeline, and ship your DevTools feature with confidence.
Remember: you are not sanitizing XPath – you are sanitizing the JavaScript literal that carries it. Keep the transport layer safe, and XPath will stay the helpful navigator it was meant to be.
This sanitization technique is already protecting users in the SiteLint – Web Audit Tools Chrome extension.
Comments