Closed Bug 1979050 Opened 1 year ago Closed 9 months ago

Import map JSON parsing code can be tricked by overwriting Object.prototype properties

Categories

(Core :: JavaScript Engine, defect, P3)

defect

Tracking

()

RESOLVED FIXED
147 Branch
Tracking Status
firefox-esr115 146+ fixed
firefox-esr140 146+ fixed
firefox145 --- wontfix
firefox146 + fixed
firefox147 + fixed

People

(Reporter: tschuster, Assigned: allstars.chh)

References

(Blocks 2 open bugs)

Details

(Keywords: csectype-other, sec-low, Whiteboard: [adv-main146-][adv-esr140.6-])

Attachments

(9 files, 2 obsolete files)

179 bytes, text/html
Details
48 bytes, text/x-phabricator-request
Details | Review
48 bytes, text/x-phabricator-request
Details | Review
48 bytes, text/x-phabricator-request
Details | Review
48 bytes, text/x-phabricator-request
Details | Review
48 bytes, text/x-phabricator-request
Details | Review
48 bytes, text/x-phabricator-request
Details | Review
48 bytes, text/x-phabricator-request
Details | Review
48 bytes, text/x-phabricator-request
Details | Review
Attached file Testcase β€”

The import map parsing code first uses JS_ParseJSON to parse the JSON string into a JS object. Afterwards it uses JS_GetProperty to lookup the properties of the object like "imports". Unfortunately JS_GetProperty behaves like Reflect.get in JS, this means it will walk the prototype chain. By modifying the Object.prototype, e.g. by adding an imports or integrity property, before the import map is parsed, the code becomes confused.

In the attached testcase, I can trick Firefox into treating "square" as a valid module path, even though the import map is empty. This doesn't reproduce in Chrome. I am not quite sure if this is also a specification issue.

Group: core-security → javascript-core-security

I just realized that if we add support for multiple import maps, it's probably going to be easier to manipulate the imports, even in a sanctioned way. This is probably low severity, unless there is something bad you could do with a getter instead of a data property.

Type: task → defect

The relevant part of the spec would be the following, especially the step 1 and step 4:

https://html.spec.whatwg.org/multipage/webappapis.html#parse-an-import-map-string

To parse an import map string, given a string input and a URL baseURL:

  1. Let parsed be the result of parsing a JSON string to an Infra value given input.
  2. If parsed is not an ordered map, then throw a TypeError indicating that the top-level value needs to be a JSON object.
  3. Let sortedAndNormalizedImports be an empty ordered map.
  4. If parsed["imports"] exists, then:
    1. If parsed["imports"] is not an ordered map, then throw a TypeError indicating that the value for the "imports" top-level key needs to be a JSON object.

The step 1 parses the JSON into an "Infra value", which does JSON.parse and then convert it into "Infra value".

https://infra.spec.whatwg.org/#parse-a-json-string-to-an-infra-value

To parse a JSON string to an Infra value, given a string string:

  1. Let jsValue be ? Call(%JSON.parse%, undefined, Β« string Β»).
  2. Return the result of converting a JSON-derived JavaScript value to an Infra value, given jsValue.

The conversion is done in the following.
The property access is done with regular "Get", but only for properties returned by [[OwnPropertyKeys]]().

https://infra.spec.whatwg.org/#convert-a-json-derived-javascript-value-to-an-infra-value

To convert a JSON-derived JavaScript value to an Infra value, given a JavaScript value jsValue:

  1. If jsValue is null, jsValue is a Boolean, jsValue is a String, or
     jsValue is a Number, then ...
  2. If IsArray(jsValue) is true:
    ...
  3. Let result be an empty ordered map.
  4. For each key of ! jsValue.[[OwnPropertyKeys]]():
    1. Let jsValueAtKey be ! Get(jsValue, key).
    2. Let infraValueAtKey be the result of converting a JSON-derived
       JavaScript value to an Infra value, given jsValueAtKey.
    3. Set result[key] to infraValueAtKey.
  5. Return result.

The remaining operations are done with the Infra value, which doesn't cause
any side-effect.

Thus, this isn't a spec bug.

https://infra.spec.whatwg.org/#map-exists

An ordered map contains an entry with a given key if there exists an entry with that key. We can also denote this by saying that, for an ordered map map and key key, "map[key] exists".

https://infra.spec.whatwg.org/#map-get

To get the value of an entry in an ordered map map given a key key and an optional default:

  1. If map does not contain key and default is given, then return default.
  2. Assert: map contains key.
  3. Return the value of the entry in map whose key is key.

Our implementation directly uses the value returned by JSON.parse.
So, there are 2 problems:

  • (a) properties on the prototypes can be reflected to the result (this bug)
  • (b) accessors on the prototypes can be triggered on the access

Then, given that the target is the result of JSON.parse, which is a plain object without any accessor on its own,
as long as we use "GetOwn"-variant, the problem (b) doesn't happen.

So, for example JS_GetOwnPropertyDescriptor could be used.

We could also use js::GetOwnPropertyPure, which isn't affected by (a) nor (b). (but currently it's internal API).

Another option is to use JS::ParseJSONWithHandler, which allow performing the "parse a JSON string to an Infra value" operations at once. Bug 1953305 has a prototype patch that implements general-purpose handler that returns C++ object tree.

Thank you arai for the analysis. Maybe we could also use the WebIDL bindings and parse the JSON into a dictionary (i.e. [GenerateInitFromJSON])? Not sure if that would cause observable differences.

(In reply to Tom Schuster (MoCo) from comment #3)

Thank you arai for the analysis. Maybe we could also use the WebIDL bindings and parse the JSON into a dictionary (i.e. [GenerateInitFromJSON])? Not sure if that would cause observable differences.

This uses a separate clean global. Why does the current import map code use the global from bug 1761938 instead of using a clean throw-away global?

(In reply to Tooru Fujisawa [:arai] from comment #2)

Bug 1953305 has a prototype patch that implements general-purpose handler that returns C++ object tree.

It looks like the purpose of bug 1953305 is to get rid of jsoncpp in Toolkit. Toolkit also seems to use parsing to serde_json::Value, and I take it that we're not trying to get rid of serde_json.

Is there a reason prefer bug 1953305 plus a tree walk in C++ to put data into a domain-specific struct over parsing to serde_json::Value plus a tree walk in Rust to put data into a domain-specific struct other than not having to make the domain-specific struct travel over FFI with cbindgen?

(In reply to Henri Sivonen (:hsivonen) from comment #4)

(In reply to Tom Schuster (MoCo) from comment #3)

Thank you arai for the analysis. Maybe we could also use the WebIDL bindings and parse the JSON into a dictionary (i.e. [GenerateInitFromJSON])? Not sure if that would cause observable differences.

This uses a separate clean global. Why does the current import map code use the global from bug 1761938 instead of using a clean throw-away global?

The spec's algorithm is not affected by polluted prototypes.
So, if we properly implement it, this can be done with single global.

Bug 1953305 has a prototype patch that implements general-purpose handler that returns C++ object tree.

It looks like the purpose of bug 1953305 is to get rid of jsoncpp in Toolkit. Toolkit also seems to use parsing to serde_json::Value, and I take it that we're not trying to get rid of serde_json.

Reducing the number of C++ JSON implementations and also improving the performance are the goal of JS::ParseJSONWithHandler API.
I don't have strong opinion for rust impl.

Is there a reason prefer bug 1953305 plus a tree walk in C++ to put data into a domain-specific struct over parsing to serde_json::Value plus a tree walk in Rust to put data into a domain-specific struct other than not having to make the domain-specific struct travel over FFI with cbindgen?

To be clear, the bug 1953305 patch is not required for this bug.

The JS::ParseJSONWithHandler itself allows consuming JSON data without creating a intermediate JS::Value object tree, but with SAX-like callbacks. This is mostly for performance reason.
(There are some other consumers of JS::ParseJSONWithHandler, without intermediate object tree, but more-target-specific data store and state management.)

But given that the state management of the callback is a bit complex, bug 1953305's struct could be used for not-performance-sensitive cases as an alternative option.

If the input data structure is simple, directly using JS::ParseJSONWithHandler will also work here.

Anyway, just using "Own" or "Pure" variant API could be a simpler fix.

Severity: -- → S3
Priority: -- → P3

I'm not entirely sure how you could abuse this, but loading unintended files because we were tricked seems like an innocent little bomb that could explode later

Keywords: sec-moderate
Assignee: nobody → allstars.chh
Group: javascript-core-security → core-security-release
Status: NEW → RESOLVED
Closed: 9 months ago
Resolution: --- → FIXED
Target Milestone: --- → 147 Branch

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

For more information, please visit BugBot documentation.

Flags: needinfo?(allstars.chh)
Attachment #9526754 - Flags: approval-mozilla-beta?

firefox-beta Uplift Approval Request

  • User impact if declined: If there's a JS injecting code that defines "imports", or "scopes" or "integrity" property on Object.prototype, later for webpages that using import maps, the module loading can fetch an unexpected module.
  • Code covered by automated testing: yes
  • Fix verified in Nightly: no
  • Needs manual QE test: no
  • Steps to reproduce for manual QE testing:
  • Risk associated with taking this patch: low
  • Explanation of risk level: Use GetOwnPropertyDescriptor to get the parsed json object's property instead of Object.prototype.
  • String changes made/needed: no
  • Is Android affected?: yes
Attachment #9526755 - Flags: approval-mozilla-beta?
Attachment #9526756 - Flags: approval-mozilla-release?

firefox-release Uplift Approval Request

  • User impact if declined: If there's a JS injecting code that defines "imports", or "scopes" or "integrity" property on Object.prototype, later for webpages that using import maps, the module loading can fetch an unexpected module.
  • Code covered by automated testing: yes
  • Fix verified in Nightly: no
  • Needs manual QE test: no
  • Steps to reproduce for manual QE testing:
  • Risk associated with taking this patch: low
  • Explanation of risk level: Use GetOwnPropertyDescriptor to get the parsed json object's property instead of Object.prototype.
  • String changes made/needed: no
  • Is Android affected?: yes
Attachment #9526759 - Flags: approval-mozilla-release?
Flags: needinfo?(allstars.chh)
Attachment #9526781 - Flags: approval-mozilla-esr140?
Attachment #9526782 - Flags: approval-mozilla-esr140?
Attachment #9526786 - Flags: approval-mozilla-esr115?
Attachment #9526787 - Flags: approval-mozilla-esr115?
Attachment #9526755 - Flags: approval-mozilla-beta? → approval-mozilla-beta+
Attachment #9526754 - Flags: approval-mozilla-beta? → approval-mozilla-beta+
Attachment #9526781 - Flags: approval-mozilla-esr140? → approval-mozilla-esr140+
Attachment #9526782 - Flags: approval-mozilla-esr140? → approval-mozilla-esr140+
QA Whiteboard: [sec] [uplift] [qa-triage-done-c147/b146]
Attachment #9526786 - Flags: approval-mozilla-esr115? → approval-mozilla-esr115+
Attachment #9526787 - Flags: approval-mozilla-esr115? → approval-mozilla-esr115+

backed out of esr 115 for causing bustage

Attachment #9526786 - Flags: approval-mozilla-esr115+ → approval-mozilla-esr115?
Attachment #9526787 - Flags: approval-mozilla-esr115+ → approval-mozilla-esr115?

(In reply to Dianna Smith [:diannaS] from comment #27)

backed out of esr 115 for causing bustage

I just noticed a typo in the ESR115 patch, I'll fix it and update the patch.

(In reply to Yoshi Cheng-Hao Huang [:allstars.chh][:allstarschh][:yoshi] from comment #28)

(In reply to Dianna Smith [:diannaS] from comment #27)

backed out of esr 115 for causing bustage

I just noticed a typo in the ESR115 patch, I'll fix it and update the patch.

Fixed and updated the ESR115 patches,
try https://treeherder.mozilla.org/jobs?repo=try&revision=a0d003000daa2e833b9d67fe241bde3a634a49aa

Attachment #9526786 - Flags: approval-mozilla-esr115? → approval-mozilla-esr115+
Attachment #9526787 - Flags: approval-mozilla-esr115? → approval-mozilla-esr115+
Attachment #9526756 - Attachment is obsolete: true
Attachment #9526756 - Flags: approval-mozilla-release?
Attachment #9526759 - Attachment is obsolete: true
Attachment #9526759 - Flags: approval-mozilla-release?
Whiteboard: [adv-main146+]
Whiteboard: [adv-main146+] → [adv-main146+][adv-esr140.6+]
Whiteboard: [adv-main146+][adv-esr140.6+] → [adv-main146-][adv-esr140.6-]
Whiteboard: [adv-main146-][adv-esr140.6-] → [adv-main146+r][adv-esr140.6+r]
Keywords: csectype-other
Whiteboard: [adv-main146+r][adv-esr140.6+r] → [adv-main146-][adv-esr140.6-]
Group: core-security-release
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Created:
Updated:
Size: