Closed Bug 2061470 Opened 3 months ago Closed 2 months ago

webRequest requestBody silently drops POST form fields named __proto__ [@ mapToObject], bypassing extension request inspection

Categories

(WebExtensions :: Request Handling, defect, P3)

defect

Tracking

(firefox157 fixed)

RESOLVED FIXED
157 Branch
Tracking Status
firefox157 --- fixed

People

(Reporter: tjr, Assigned: robwu)

References

Details

(Keywords: ai-involved, csectype-bounds, testcase, Whiteboard: [addons-jira][adv-main157-])

Attachments

(5 files, 1 external file)

Note: While the impact as described is minimal, overwriting the proto is concerning.

WebRequestUpload.createRequestBody parses urlencoded and multipart POST bodies into a Map and converts it to a plain object with mapToObject (toolkit/components/extensions/webrequest/WebRequestUpload.sys.mjs:128-134), which assigns result[key] = value on a {} literal. For a field named "proto" (whose parsed value is an Array), the assignment invokes the inherited Object.prototype.proto accessor-setter, swapping the object's prototype instead of creating an own property. The field therefore never appears as an own enumerable property and is absent from JSON.stringify, Object.keys, and the structured clone delivered to extensions. Because the parse itself succeeds (parseUrlEncoded:381-393 and parseMultiPart:315-372 store "proto" as an ordinary Map key), createRequestBody (:522-546) returns { formData } and never produces the raw/lenientFormData fallback that the API provides on parse failure. The result is a zero-trace suppression: any extension listening with webRequest.onBeforeRequest and extraInfoSpec ["blocking","requestBody"] (ad blockers, DLP/anti-exfiltration tools, request loggers) sees neither the field nor any raw bytes, while any ordinary HTTP backend receives and parses the field normally. Reproduced on a fresh ASAN build at ca6e9493686b: for control requests the observer extension saw the sentinel; for attack requests naming the field proto (both application/x-www-form-urlencoded and multipart/form-data) the extension saw formData={"a":["1"]} with no own proto key and no raw key, while the Python backend logged {"a": ["1"], "proto": ["CLAUDITOR_SECRET_7f3a9c1e"]} for the same request. The prototype swap does not escalate further: the object is consumed via structured clone (own enumerable properties only), so only the inspection-bypass effect results.

Build Info

Affected Code

File: toolkit/components/extensions/webrequest/WebRequestUpload.sys.mjs, line 128-134

function mapToObject(map) {
  let result = {};
  for (let [key, value] of map) {
    result[key] = value; // key "__proto__" hits the inherited Object.prototype.__proto__ setter: prototype swap, no own property
  }
  return result;
}

File: toolkit/components/extensions/webrequest/WebRequestUpload.sys.mjs, line 522-546

createRequestBody(channel) {
  ...
    let formData = createFormData(stream, channel);
    if (formData) {
      return { formData }; // parse succeeded: no raw fallback is ever attached
    }

    // If we failed to parse the stream as form data, return it as a
    // sequence of raw data chunks, along with a leniently-parsed form
    // data object, which ignores encoding errors.
    return {
      raw: Array.from(getRawDataChunked(stream)),
      lenientFormData: createFormData(stream, channel, true),
    };
  ...

File: toolkit/components/extensions/webrequest/WebRequest.sys.mjs, line 940-945

if (opts.requestBody) {
  requestBody =
    requestBody ||
    lazy.WebRequestUpload.createRequestBody(channel.channel);
  data.requestBody = requestBody;
}

mapToObject performs the lossy assignment; createRequestBody returns formData without a raw fallback because the parse succeeded; WebRequest.sys.mjs attaches the object as data.requestBody for listeners with the requestBody opt.

Exploit Chain

  1. Attacker web content (or any page-authored form/fetch) issues a POST whose body contains a field named proto carrying the data to hide, in application/x-www-form-urlencoded or multipart/form-data encoding.
  2. In the parent process, WebRequest.sys.mjs calls WebRequestUpload.createRequestBody for any extension listener registered with extraInfoSpec ["blocking","requestBody"].
  3. parseUrlEncoded/parseMultiPart parse the body successfully, storing "proto" as an ordinary Map key.
  4. mapToObject copies the Map into a plain {}; the assignment for key "proto" triggers the inherited Object.prototype.proto setter, so no own property is created and the field disappears from every own-property view.
  5. createRequestBody returns { formData } (parse success), so no raw or lenientFormData fallback accompanies the object.
  6. The extension receives requestBody via structured clone with neither the field nor raw bytes; inspection, logging, and blocking logic based on requestBody cannot observe the data.
  7. The unmodified HTTP request reaches the network; any standard backend parser (demonstrated with Python's parse_qs and email multipart parser) receives proto as an ordinary parameter, completing the exfiltration/evasion.

Steps to Reproduce

  1. Start from a clean mozilla-central checkout (verified at ca6e9493686b).
  2. git apply source_patch.diff — adds a minimal built-in observer WebExtension (browser/extensions/clauditor-poc/) that registers webRequest.onBeforeRequest with ["blocking","requestBody"] and only logs what the API delivers; it stands in for any user-installed request-inspection extension and uses only public WebExtension APIs.
  3. Build with build_firefox using mozconfig.linux.asan.fuzzing.
  4. Start the capture backend: python3 external_services/echo_server.py (listens on 127.0.0.1:38713).
  5. Run testcase.html via browser_evaluator with no custom prefs and timeout 45.
  6. Observe in logs.stderr: control cases show the sentinel in the extension's requestBody view (sentinelVisible=true); attack cases show formData={"a":["1"]} with hasOwnProto=false hasRaw=false sentinelVisible=false and POC-CONFIRMED, while SERVER-PARSED/SERVER-RECEIVED lines show the backend received proto=CLAUDITOR_SECRET_7f3a9c1e for the same requests.

Security Impact

  • Severity: Low
  • Attacker capability: Web content can smuggle the contents of an arbitrary POST form field past all webRequest requestBody-based inspection and blocking (ad blockers, DLP/anti-exfiltration extensions, request loggers) by naming the field proto, in both urlencoded and multipart encodings, with no trace in formData and no raw fallback, while any standard HTTP backend receives the field as an ordinary parameter. No memory corruption, no process-boundary crossing, and no data the attacker does not already hold is disclosed; the impact is evasion of extension-based request inspection.
  • Preconditions: The user has an extension installed that inspects or filters request bodies via webRequest with the requestBody extraInfoSpec (the victim class of the finding); the attacker only needs to control the body of a POST request.

ASAN Report

Non-crash logic finding (crashed:false expected). Proof captured from browser_evaluator logs.stderr on the verifier's independent run:

[Child 14405] E/console error: "POC-OBSERVER-READY"
[Child 14405] E/console error: "WEBREQUEST-VIEW case=control-0 requestBody={\"formData\":{\"a\":[\"1\"],\"plainname\":[\"CLAUDITOR_SECRET_7f3a9c1e\"]}}"
[Child 14405] E/console error: "WEBREQUEST-KEYS case=control-0 keys=[\"a\",\"plainname\"] hasOwnProto=false hasRaw=false sentinelVisible=true"
[Child 14405] E/console error: "WEBREQUEST-VIEW case=attack-urlencoded-0 requestBody={\"formData\":{\"a\":[\"1\"]}}"
[Child 14405] E/console error: "WEBREQUEST-KEYS case=attack-urlencoded-0 keys=[\"a\"] hasOwnProto=false hasRaw=false sentinelVisible=false"
[Child 14405] E/console error: "POC-CONFIRMED: CLAUDITOR_SECRET_7f3a9c1e sent in POST field named __proto__ is invisible to webRequest requestBody inspection (case=attack-urlencoded-0, formData={\"a\":[\"1\"]})"
[Child 14480] E/console error: "SERVER-PARSED case=attack-urlencoded-0 fields={\"a\":[\"1\"],\"__proto__\":[\"CLAUDITOR_SECRET_7f3a9c1e\"]}"
[Child 14405] E/console error: "WEBREQUEST-VIEW case=attack-multipart-0 requestBody={\"formData\":{\"a\":[\"1\"]}}"
[Child 14405] E/console error: "POC-CONFIRMED: CLAUDITOR_SECRET_7f3a9c1e sent in POST field named __proto__ is invisible to webRequest requestBody inspection (case=attack-multipart-0, formData={\"a\":[\"1\"]})"
(pattern repeated for all 20 iterations, urlencoded and multipart)

External Python backend log (same requests, server-side ground truth):
SERVER-RECEIVED /poc-submit?case=control-0&sentinel=CLAUDITOR_SECRET_7f3a9c1e {"a": ["1"], "plainname": ["CLAUDITOR_SECRET_7f3a9c1e"]}
SERVER-RECEIVED /poc-submit?case=attack-urlencoded-0&sentinel=CLAUDITOR_SECRET_7f3a9c1e {"a": ["1"], "__proto__": ["CLAUDITOR_SECRET_7f3a9c1e"]}
SERVER-RECEIVED /poc-submit?case=attack-multipart-0&sentinel=CLAUDITOR_SECRET_7f3a9c1e {"a": ["1"], "__proto__": ["CLAUDITOR_SECRET_7f3a9c1e"]}
Attached file crash_stack.txt —
Attached file echo_server.py —
Attached patch fix.patch — — Splinter Review
Attached file testcase.html —

This bug was filed automatically by an AI-powered security testing tool. The analysis, testcase, and any suggested patches were generated by an AI model and have NOT been verified by a human.

Please review all details carefully before triaging. The issue was confirmed by an independent verifier, but for non-ASAN issues, the verifier has been made less strict. Additionally, the root cause analysis and suggested fix may contain errors.

Group: core-security → firefox-core-security

The severity field is not set for this bug.
:robwu, could you have a look please?

For more information, please visit BugBot documentation.

Flags: needinfo?(rob)

(In reply to Tom Ritter [:tjr] from comment #0)

Note: While the impact as described is minimal, overwriting the proto is concerning.

Why is this a security bug? I would consider this just a regular bug.
And I would fix it by removing the mapToObject function and replacing its only caller with Object.fromEntries.

Severity: -- → S3
Flags: needinfo?(rob) → needinfo?(tom)
Priority: -- → P3

If you're confident it has no security implications, that's fine, you can declassify it.

Flags: needinfo?(tom)

I'll open it up and attach a patch to it since it is a valid bug report and trivial to fix.

Group: firefox-core-security
Assignee: nobody → rob
Status: UNCONFIRMED → ASSIGNED
Ever confirmed: true
Status: ASSIGNED → RESOLVED
Closed: 2 months ago
Resolution: --- → FIXED
Target Milestone: --- → 157 Branch
Whiteboard: [addons-jira]
Whiteboard: [addons-jira] → [addons-jira][adv-main157+]
Alias: CVE-2026-100827
Keywords: sec-low
Whiteboard: [addons-jira][adv-main157+] → [addons-jira][adv-main157-]
Alias: CVE-2026-100827
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Created:
Updated:
Size: