Closed Bug 1130028 Opened 10 years ago Closed 9 years ago

Security Exception when defining a custom element in a FxOS addon

Categories

(Core :: DOM: Device Interfaces, defect, P1)

x86
macOS
defect

Tracking

()

RESOLVED FIXED
mozilla39
Tracking Status
firefox39 --- fixed

People

(Reporter: kgrandon, Assigned: wchen)

References

Details

(Whiteboard: [spark])

Attachments

(3 files, 3 obsolete files)

We see the following error when trying to create a custom element in a FirefoxOS addon:

Error sandboxing app://app/script.js : SecurityError: The operation is insecure.


This happens as soon as you call document.registerElement();
I've learned that if the origin of the prototype for create element differs from the page, we may run into security exceptions.

Public spec: https://www.w3.org/Bugs/Public/show_bug.cgi?id=27260
Spoke to wchen over IRC, the exception is possibly being thrown here: https://dxr.mozilla.org/mozilla-central/source/dom/base/nsDocument.cpp#6204

It's possible that the script loader put the prototype in a compartment different from the page and when we try to access the prototype from the page's compartment we can't because there's a security policy in place on the wrapper (https://dxr.mozilla.org/mozilla-central/source/js/src/proxy/Wrapper.cpp#119).
William, could you take a look at this?
Flags: needinfo?(wchen)
Priority: -- → P1
Whiteboard: [lightsaber]
Kevin, do you think you could provide a test case that I can debug?
Flags: needinfo?(wchen) → needinfo?(kgrandon)
Attached file [Fxos-Customizer] Addon which causes security error (obsolete) (deleted) —
Here is a packaged addon which causes the error to appear. The easiest way to see this effect is if you unpack this addon to the outoftree_apps/ folder inside of a gaia checkout. You may need to create the outoftree_apps/ folder if it doesn't exist. Assuming you have a gaia checkout inside of a folder named 'gaia', when unpacked the folder should live somewhere like:

gaia/outoftree_apps/fxos-customizer/

There should be a manifest file at:

gaia/outoftree_apps/fxos-customizer/manifest.webapp
Flags: needinfo?(kgrandon)
Here is a new build of the customizer which works with the gecko patch. I wasn't able to get it working with all apps, but it did work on the contacts and messaging apps. I'll work with the gaia-devs to start looking into some of these issues.
Attachment #8569463 - Attachment is obsolete: true
Attached patch Possible fix (obsolete) (deleted) — Splinter Review
So the situation we have is that the add-on script is executed with an extended principal giving it more privilege than the app. The current registerElement implementation throws a security exception for prototypes that can't be seen from the document's compartment (in this case, it's because the app's principal is less privileged than the principal for the add-on script).

It's actually not to clear what we should do here. In the attached patch, I've changed the implementation to allow the prototype from the add-on script and elements that are created will have the registered prototype. However, the prototype will only be visible from the add-on script and it will not be visible from the app. This means that any API that you added on the prototype won't be available to scripts in the app.
bholley: How do you think we should behave when we call registerElement with a privileged prototype?
Flags: needinfo?(bobbyholley)
So. It all really depends on what we really want and how much work we want to do here.

If the addon wants to create a custom element accessible from content _and_ implemented in content (i.e. no security boundary), it should just use window.eval('{...}') to create the custom prototype. In this case, the custom prototype is not going to be visible at all to the addon (and the XrayWrapper will cause the prototype to go directly to HTMLCustomElement.prototype, I believe).

If the addon wants to create a custom element accessible from content _but_ implemented in the addon scope (i.e. with a security boundary), then it should create an content-sideAPI object using exportFunction (or, more conveniently, cloneInto(apiObj, window, {cloneFunctions: true}). If |apiObj| only has accessors and methods (and any underlying data is stored on a different object), then cloning it into content will create a content-side object with a bunch of content-side functions that forward to the addon-side functions (make sure that you use a closure to access your data and not |this|, since |this| can't be trusted). This should work for the content, but still won't make the custom prototype visible to code running in the addon scope.

If we want to make custom elements with prototypes that are usable from the addon (but not from content), then we need:
(1) Code to deal with the fact that the prototype might not be same-compartment with the document (i.e. something along the lines of wchen's patch above)
(2) Xray support to report the proper prototype to XrayWrappers.

If we want to make custom elements with prototypes that are usable from both the addon _and_ content, things get tricky. The basic issue is that there isn't an easy way to make an arbitrary user-create JS object (the prototype) work naturally for both foo.com and nsEP([foo.com]) simultaneously. There's not really any way to Xray to these objects (since there's nothing to Xray _to_, they're just regular Objects). There are various complicated ways we could try to kinda-sorta support this, but I don't want to spend time brainstorming it unless we actually want this badly enough to spend a week or two of someone's time making it work.
Flags: needinfo?(bobbyholley)
Comment on attachment 8571460 [details] [diff] [review]
Possible fix

Review of attachment 8571460 [details] [diff] [review]:
-----------------------------------------------------------------

::: dom/base/nsDocument.cpp
@@ +6207,5 @@
>      } else {
>        protoObject = aOptions.mPrototype;
>  
> +      {
> +        JSAutoCompartment ac(aCx, protoObject);

Before doing this, you _need_ to make sure that the caller subsumes the underlying object (after unwrapping it). Otherwise, the caller could pass a cross-origin object, it would get re-wrapped into the content compartment, and then the content document has no way of knowing whether the caller was allowed to access the underlying object or not.

@@ +6221,5 @@
>  
> +        JS::Rooted<JSPropertyDescriptor> descRoot(aCx);
> +        JS::MutableHandle<JSPropertyDescriptor> desc(&descRoot);
> +        // This check will go through a wrapper, but as we checked above
> +        // it should be transparent or an xray. This should be fine for now,

Where does this check happen?
In this particular case we want the custom element to work in the add-on.

bholley: Can you review this? or perhaps reassign to mrbkap.
Attachment #8571460 - Attachment is obsolete: true
Attachment #8572281 - Flags: review?(bobbyholley)
Assignee: nobody → kgrandon
Assignee: kgrandon → wchen
Comment on attachment 8572281 [details] [diff] [review]
Set registered prototype in compartment of caller of registerElement

Review of attachment 8572281 [details] [diff] [review]:
-----------------------------------------------------------------

This needs tests. Write a mochitest-chrome with a content iframe and a Sandbox with nsEP(iframePrincipal), and try doing web components stuff on it.

::: dom/base/Element.cpp
@@ +420,5 @@
>      nsDocument* document = static_cast<nsDocument*>(OwnerDoc());
>      JS::Rooted<JSObject*> prototype(aCx);
>      document->GetCustomPrototype(NodeInfo()->NamespaceID(), data->mType, &prototype);
>      if (prototype) {
> +      // We want to set custom prototype in the compartment where the prototype

"We want to set the custom prototype in the compartment where it was registered".

@@ +424,5 @@
> +      // We want to set custom prototype in the compartment where the prototype
> +      // was registered so that the custom prototype is visible to custom
> +      // element callbacks in the compartment where registration occurred.
> +      JSAutoCompartment ac(aCx, prototype);
> +      if (!JS_WrapObject(aCx, &obj) || !JS_SetPrototype(aCx, obj, prototype)) {

So what this is doing is setting a custom "expando" prototype on the XrayWrapper that is only visible from the origin that set it. This isn't what I had in mind, but actually might work, assuming that it's really ok for the prototype to only exist from the XrayWrapper's perspective and not that of the underlying element (or XrayWrappers from any other origins).

This seems just crazy enough to work, but I'm worried that it might break underlying C++ invariants in the web components code. Blake?

::: dom/base/nsDocument.cpp
@@ +6235,5 @@
>          return;
>        }
>  
> +      // Enter the compartment of the protoObject just for some checks.
> +      JSAutoCompartment ac(aCx, protoObject);

You're still in the original compartment of protoObject, right? I don't see us unwrapping it anywhere, so this ac seems like a no-op.

@@ +6342,5 @@
>    // Associate the definition with the custom element.
>    CustomElementHashKey key(namespaceID, typeAtom);
>    LifecycleCallbacks* callbacks = callbacksHolder.forget();
>    CustomElementDefinition* definition =
> +    new CustomElementDefinition(wrappedProto,

Here it looks like we're storing the prototype wrapped into the document's compartment, which contradicts the comment in nsDocument.h. What's the story?

@@ +6378,1 @@
>          if (!JS_SetPrototype(aCx, wrapper, protoObject)) {

This needs a comment explaining that we may be only setting the prototype on an XrayWrapper for the element (here and in the code in Element.cpp).
Attachment #8572281 - Flags: review?(mrbkap)
Attachment #8572281 - Flags: review?(bobbyholley)
Attachment #8572281 - Flags: feedback+
Oh, also - how are createdCallback etc going to work? I don't see any code to handle crossing compartment boundaries in that case.
(In reply to Bobby Holley (:bholley) from comment #12)
> Comment on attachment 8572281 [details] [diff] [review]
> Here it looks like we're storing the prototype wrapped into the document's
> compartment, which contradicts the comment in nsDocument.h. What's the story?
> 
I think we're in the caller's compartment when we call JS_WrapObject(aCx, &wrappedProto)

> Oh, also - how are createdCallback etc going to work? I don't see any code
> to handle crossing compartment boundaries in that case.
The callbacks are WebIDL CallbackFunction so I think the bindings should handle that when we call |Call| on them.
Attachment #8572281 - Attachment is obsolete: true
Attachment #8572281 - Flags: review?(mrbkap)
Attachment #8572929 - Flags: review?(mrbkap)
Attached patch v1 diff v2 (deleted) — Splinter Review
Status: NEW → ASSIGNED
Comment on attachment 8572930 [details] [diff] [review]
v1 diff v2

With both attachment 8572930 [details] [diff] [review] and attachment 8571460 [details] [diff] [review], I'm getting this error under the same circumstances:
W/GeckoConsole(13025): [JavaScript Error: "NotSupportedError: Operation is not supported" {file: "app://ac1b484a-843c-fd4f-8244-8348a4479ce4/js/addon-concat.js" line: 1545}]

This is that line:
```js
module.exports = document.registerElement('gaia-dialog', { prototype: proto });
```
(In reply to Doug Sherk (:drs) (use needinfo?) from comment #16)
> Comment on attachment 8572930 [details] [diff] [review]
> v1 diff v2
> 
> With both attachment 8572930 [details] [diff] [review] and attachment
> 8571460 [details] [diff] [review], I'm getting this error under the same
> circumstances:
> W/GeckoConsole(13025): [JavaScript Error: "NotSupportedError: Operation is
> not supported" {file:
> "app://ac1b484a-843c-fd4f-8244-8348a4479ce4/js/addon-concat.js" line: 1545}]

That's a different error likely caused by a different issue (my guess is that gaia-dialog was already registered earlier by something else?). I can take a look if you upload the add-on.
(In reply to William Chen [:wchen] from comment #17)
> That's a different error likely caused by a different issue (my guess is
> that gaia-dialog was already registered earlier by something else?). I can
> take a look if you upload the add-on.

The only way to get past this was to comment out every `document.registerElement()` call, so I don't think any elements are being registered more than once.

Here's the app:
http://people.mozilla.org/~dsherk/customizer.zip

You can unzip this and then install it using WebIDE. Make sure that you go into Settings > Addons and enable this add-on. The errors should pop up right away in |adb logcat| once enabled.
Flags: needinfo?(wchen)
When I tested the patch it was working for me... It seems to be a different issue. In order to not conflate things, should we track this in a different bug?
Comment on attachment 8572929 [details] [diff] [review]
Set registered prototype in compartment of caller of registerElement. v2

Review of attachment 8572929 [details] [diff] [review]:
-----------------------------------------------------------------

r=me with an answer to the question of what to do in the case of failures during the element upgrade algorithm.

::: dom/base/Element.cpp
@@ +425,5 @@
> +      // registered. In the case that |obj| and |prototype| are in different
> +      // compartments, this will set the prototype on the |obj|'s wrapper and
> +      // thus only visible in the wrapper's compartment.
> +      JSAutoCompartment ac(aCx, prototype);
> +      if (!JS_WrapObject(aCx, &obj) || !JS_SetPrototype(aCx, obj, prototype)) {

Overwriting obj here means that we'll return the wrapper instead of the object itself. I'd prefer to use a temporary to do this SetPrototype.

::: dom/base/nsDocument.cpp
@@ +6376,3 @@
>        JS::RootedObject wrapper(aCx);
> +      if ((wrapper = cache->GetWrapper()) && JS_WrapObject(aCx, &wrapper)) {
> +        if (!JS_SetPrototype(aCx, wrapper, wrappedProto)) {

Nit: You can combine the two if statements with && here.

Non-nit: if JS_WrapObject or JS_SetPrototype fail, then we need to propagate the error back out to the caller.

The spec doesn't deal with the case of failure during this bit of the algorithm, but we probably need to do something if we throw an exception during either the JS_WrapObject or JS_SetPrototype calls. Right now, we risk leaving an exception on aCx. At the very least, we should probably call JS_ClearPendingException or JS_ReportPendingException in the case of failure.
Attachment #8572929 - Flags: review?(mrbkap) → review+
(In reply to Blake Kaplan (:mrbkap) (please use needinfo!) from comment #20)
> The spec doesn't deal with the case of failure during this bit of the
> algorithm, but we probably need to do something if we throw an exception
> during either the JS_WrapObject or JS_SetPrototype calls. Right now, we risk
> leaving an exception on aCx. At the very least, we should probably call
> JS_ClearPendingException or JS_ReportPendingException in the case of failure.

For any new error handling stuff, please use the API on AutoJSAPI by invoking TakeOwnershipOfErrorReporting and then either clearing the exception, stealing it, or letting the AutoJSAPI destructor report it.
Ah-ha. I guess that in that case we'll need to add an AutoJSAPI and use it here.
(In reply to Blake Kaplan (:mrbkap) (please use needinfo!) from comment #22)
> Ah-ha. I guess that in that case we'll need to add an AutoJSAPI and use it
> here.

Where is the cx coming from? If it's being passed in from DOM bindings, then you don't need to do anything - just propagate the exception.
The basic idea is that you instantiate an AutoJSAPI when you want to enter JS with a clean slate. Stuff like registerElement inherits the JS state from the JS caller, and thus doesn't need to do anything fancy with exceptions.
I think that the trickiness is that we don't abort the algorithm at the first exception. Right now, we'll happily call into the JSAPI with an exception pending from a previous operation, which (AFAIK) is not allowed. I think that means that we need to explicitly clear the exception if one is thrown.
(In reply to Blake Kaplan (:mrbkap) (please use needinfo!) from comment #25)
> I think that the trickiness is that we don't abort the algorithm at the
> first exception. Right now, we'll happily call into the JSAPI with an
> exception pending from a previous operation, which (AFAIK) is not allowed. I
> think that means that we need to explicitly clear the exception if one is
> thrown.

Oh I see. Yes, JS_ClearPendingException is an OK thing to do I guess. Long-term we'll be passing around AutoJSAPI& / AutoEntryScript& instead of JSContext* (or accessing them ambiently), so we then we can just invoke ClearPendingException on the JSContext itself.
(In reply to Bobby Holley (:bholley) from comment #26)
> so we then we can just invoke
> ClearPendingException on the JSContext itself.

Er, on the AutoJSAPI itself.
Hey William,

Any update on when this can land?
https://hg.mozilla.org/integration/mozilla-inbound/rev/ceda5e90d080

(In reply to Blake Kaplan (:mrbkap) (please use needinfo!) from comment #20)
> At the very least, we should probably call
> JS_ClearPendingException or JS_ReportPendingException in the case of failure.

Addressed review comments and used JS_ReportPendingException.
Flags: needinfo?(wchen) → in-testsuite+
Doesn't this patch change the return value of Element::WrapObject to be in the custom proto compartment?  That's not what the contract for this function is...

I guess I'll fix that up in bug 1145017.
https://hg.mozilla.org/mozilla-central/rev/ceda5e90d080
Status: ASSIGNED → RESOLVED
Closed: 9 years ago
Resolution: --- → FIXED
Target Milestone: --- → mozilla39
(In reply to Ryan VanderMeulen [:RyanVM UTC-4] from comment #32)
> https://hg.mozilla.org/integration/b2g-inbound/rev/d8c689def44e

This was from bug 1135293, which had the wrong bug number in the commit message.
Whiteboard: [lightsaber] → [spark]
Blocks: spark-addons
No longer blocks: spark
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Created:
Updated:
Size: