Bug 1297687 - Part 3: Ensure that the expanded principal of a sandbox has a sensible OriginAttributes; r=bholley a=ritu
authorEhsan Akhgari <ehsan@mozilla.com>
Fri, 09 Sep 2016 18:59:05 -0400
changeset 350448 9431fe63e758b2beeee367c82b620be4d9a4fb7e
parent 350447 06dd44643f5328db2ff949fe35fd20b0b04caaac
child 350449 2c494a5c88a4af5b258893eb48f1416052cfaa06
push id1230
push userjlund@mozilla.com
push dateMon, 31 Oct 2016 18:13:35 +0000
treeherdermozilla-release@5e06e3766db2 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbholley, ritu
bugs1297687
milestone50.0
Bug 1297687 - Part 3: Ensure that the expanded principal of a sandbox has a sensible OriginAttributes; r=bholley a=ritu This patch allows specifying an OriginAttributes when creating a sandbox using Components.utils.Sandbox() by specifying an originAttributes member on the options dictionary. If an OA is specified in this way, it is used for creating codebase principals from the string arguments passed to the function. Otherwise, if one or more principals are passed in the array argument to Sandbox(), the OA of the principal(s) is used to construct codebase principals from the strings inside the array. In this case, we check to make sure that all of the passed principals have the same OA, otherwise we'll throw an exception. In case no explicit OA is specified and no principals are passed in the array argument, we create the codebase principals using a default OA.
caps/tests/unit/test_origin.js
js/xpconnect/src/Sandbox.cpp
js/xpconnect/src/xpcprivate.h
toolkit/components/extensions/ExtensionContent.jsm
--- a/caps/tests/unit/test_origin.js
+++ b/caps/tests/unit/test_origin.js
@@ -30,16 +30,23 @@ function checkOriginAttributes(prin, att
   do_check_true(ChromeUtils.originAttributesMatchPattern(prin.originAttributes, attrs));
   if (!prin.isNullPrincipal && !prin.origin.startsWith('[')) {
     do_check_true(ssm.createCodebasePrincipalFromOrigin(prin.origin).equals(prin));
   } else {
     checkThrows(() => ssm.createCodebasePrincipalFromOrigin(prin.origin));
   }
 }
 
+function checkSandboxOriginAttributes(arr, attrs, options) {
+  options = options || {};
+  var sandbox = Cu.Sandbox(arr, options);
+  checkOriginAttributes(Cu.getObjectPrincipal(sandbox), attrs,
+                        ChromeUtils.originAttributesToSuffix(attrs));
+}
+
 // utility function useful for debugging
 function printAttrs(name, attrs) {
   do_print(name + " {\n" +
            "\tappId: " + attrs.appId + ",\n" +
            "\tuserContextId: " + attrs.userContextId + ",\n" +
            "\tinIsolatedMozBrowser: " + attrs.inIsolatedMozBrowser + ",\n" +
            "\taddonId: '" + attrs.addonId + "',\n" +
            "\tsignedPkg: '" + attrs.signedPkg + "'\n}");
@@ -163,16 +170,40 @@ function run_test() {
   // signedPkg and browser
   var exampleOrg_signedPkg_browser = ssm.createCodebasePrincipal(makeURI('http://example.org'), {signedPkg: 'whatever', inIsolatedMozBrowser: true});
   checkOriginAttributes(exampleOrg_signedPkg_browser, { signedPkg: 'whatever', inIsolatedMozBrowser: true }, '^inBrowser=1&signedPkg=whatever');
   do_check_eq(exampleOrg_signedPkg_browser.origin, 'http://example.org^inBrowser=1&signedPkg=whatever');
 
   // Just signedPkg (but different value from 'exampleOrg_signedPkg_app')
   var exampleOrg_signedPkg_another = ssm.createCodebasePrincipal(makeURI('http://example.org'), {signedPkg: 'whatup'});
 
+  checkSandboxOriginAttributes(null, {});
+  checkSandboxOriginAttributes('http://example.org', {});
+  checkSandboxOriginAttributes('http://example.org', {}, {originAttributes: {}});
+  checkSandboxOriginAttributes('http://example.org', {appId: 42}, {originAttributes: {appId: 42}});
+  checkSandboxOriginAttributes(['http://example.org'], {});
+  checkSandboxOriginAttributes(['http://example.org'], {}, {originAttributes: {}});
+  checkSandboxOriginAttributes(['http://example.org'], {appId: 42}, {originAttributes: {appId: 42}});
+  checkSandboxOriginAttributes([exampleOrg_signedPkg, 'http://example.org'], {signedPkg: 'whatever'});
+  checkSandboxOriginAttributes(['http://example.org', exampleOrg_signedPkg], {signedPkg: 'whatever'});
+  checkSandboxOriginAttributes(['http://example.org', exampleOrg_app, exampleOrg_signedPkg], {signedPkg: 'whatever'}, {originAttributes: {signedPkg: 'whatever'}});
+  checkSandboxOriginAttributes(['http://example.org', exampleOrg_signedPkg, exampleOrg_app], {signedPkg: 'whatever'}, {originAttributes: {signedPkg: 'whatever'}});
+  checkSandboxOriginAttributes([exampleOrg_app, exampleOrg_signedPkg, 'http://example.org'], {signedPkg: 'whatever'}, {originAttributes: {signedPkg: 'whatever'}});
+  checkSandboxOriginAttributes([exampleOrg_app, 'http://example.org', exampleOrg_signedPkg], {signedPkg: 'whatever'}, {originAttributes: {signedPkg: 'whatever'}});
+  checkSandboxOriginAttributes([exampleOrg_signedPkg, exampleOrg_app, 'http://example.org'], {signedPkg: 'whatever'}, {originAttributes: {signedPkg: 'whatever'}});
+  checkSandboxOriginAttributes([exampleOrg_signedPkg, 'http://example.org', exampleOrg_app], {signedPkg: 'whatever'}, {originAttributes: {signedPkg: 'whatever'}});
+  checkThrows(() => Cu.Sandbox([exampleOrg_app, exampleOrg_signedPkg]));
+  checkThrows(() => Cu.Sandbox([exampleOrg_signedPkg, exampleOrg_app]));
+  checkThrows(() => Cu.Sandbox(['http://example.org', exampleOrg_app, exampleOrg_signedPkg]));
+  checkThrows(() => Cu.Sandbox(['http://example.org', exampleOrg_signedPkg, exampleOrg_app]));
+  checkThrows(() => Cu.Sandbox([exampleOrg_app, exampleOrg_signedPkg, 'http://example.org']));
+  checkThrows(() => Cu.Sandbox([exampleOrg_app, 'http://example.org', exampleOrg_signedPkg]));
+  checkThrows(() => Cu.Sandbox([exampleOrg_signedPkg, exampleOrg_app, 'http://example.org']));
+  checkThrows(() => Cu.Sandbox([exampleOrg_signedPkg, 'http://example.org', exampleOrg_app]));
+
   // Check that all of the above are cross-origin.
   checkCrossOrigin(exampleOrg_app, exampleOrg);
   checkCrossOrigin(exampleOrg_app, nullPrin_app);
   checkCrossOrigin(exampleOrg_browser, exampleOrg_app);
   checkCrossOrigin(exampleOrg_browser, nullPrin_browser);
   checkCrossOrigin(exampleOrg_appBrowser, exampleOrg_app);
   checkCrossOrigin(exampleOrg_appBrowser, nullPrin_appBrowser);
   checkCrossOrigin(exampleOrg_appBrowser, exampleCom_appBrowser);
--- a/js/xpconnect/src/Sandbox.cpp
+++ b/js/xpconnect/src/Sandbox.cpp
@@ -1301,17 +1301,18 @@ GetPrincipalOrSOP(JSContext* cx, HandleO
     return true;
 }
 
 /*
  * The first parameter of the sandbox constructor might be an array of principals, either in string
  * format or actual objects (see GetPrincipalOrSOP)
  */
 static bool
-GetExpandedPrincipal(JSContext* cx, HandleObject arrayObj, nsIExpandedPrincipal** out)
+GetExpandedPrincipal(JSContext* cx, HandleObject arrayObj,
+                     const SandboxOptions& options, nsIExpandedPrincipal** out)
 {
     MOZ_ASSERT(out);
     uint32_t length;
 
     if (!JS_GetArrayLength(cx, arrayObj, &length))
         return false;
     if (!length) {
         // We need a whitelist of principals or uri strings to create an
@@ -1319,61 +1320,132 @@ GetExpandedPrincipal(JSContext* cx, Hand
         // report error.
         JS_ReportError(cx, "Expected an array of URI strings");
         return false;
     }
 
     nsTArray< nsCOMPtr<nsIPrincipal> > allowedDomains(length);
     allowedDomains.SetLength(length);
 
+    // If an originAttributes option has been specified, we will use that as the
+    // OriginAttribute of all of the string arguments passed to this function.
+    // Otherwise, we will use the OriginAttributes of a principal or SOP object
+    // in the array, if any.  If no such object is present, and all we have are
+    // strings, then we will use a default OriginAttribute.
+    // Otherwise, we will use the origin attributes of the passed object(s). If
+    // more than one object is specified, we ensure that the OAs match.
+    Maybe<PrincipalOriginAttributes> attrs;
+    if (options.originAttributes) {
+        attrs.emplace();
+        JS::RootedValue val(cx, JS::ObjectValue(*options.originAttributes));
+        if (!attrs->Init(cx, val)) {
+            // The originAttributes option, if specified, must be valid!
+            JS_ReportError(cx, "Expected a valid OriginAttributes object");
+            return false;
+        }
+    }
+
+    // Now we go over the array in two passes.  In the first pass, we ignore
+    // strings, and only process objects.  Assuming that no originAttributes
+    // option has been passed, if we encounter a principal or SOP object, we
+    // grab its OA and save it if it's the first OA encountered, otherwise
+    // check to make sure that it is the same as the OA found before.
+    // In the second pass, we ignore objects, and use the OA found in pass 0
+    // (or the previously computed OA if we have obtained it from the options)
+    // to construct codebase principals.
+    //
+    // The effective OA selected above will also be set as the OA of the
+    // expanded principal object.
+
+    // First pass:
     for (uint32_t i = 0; i < length; ++i) {
         RootedValue allowed(cx);
         if (!JS_GetElement(cx, arrayObj, i, &allowed))
             return false;
 
         nsresult rv;
         nsCOMPtr<nsIPrincipal> principal;
-        if (allowed.isString()) {
-            // In case of string let's try to fetch a codebase principal from it.
-            RootedString str(cx, allowed.toString());
-
-            // We use a default originAttributes here because we don't support
-            // passing a userContextId with an array.
-            PrincipalOriginAttributes attrs;
-            if (!ParsePrincipal(cx, str, attrs, getter_AddRefs(principal)))
-                return false;
-
-        } else if (allowed.isObject()) {
+        if (allowed.isObject()) {
             // In case of object let's see if it's a Principal or a ScriptObjectPrincipal.
             nsCOMPtr<nsISupports> prinOrSop;
             RootedObject obj(cx, &allowed.toObject());
             if (!GetPrincipalOrSOP(cx, obj, getter_AddRefs(prinOrSop)))
                 return false;
 
             nsCOMPtr<nsIScriptObjectPrincipal> sop(do_QueryInterface(prinOrSop));
             principal = do_QueryInterface(prinOrSop);
             if (sop)
                 principal = sop->GetPrincipal();
-        }
-        NS_ENSURE_TRUE(principal, false);
+            NS_ENSURE_TRUE(principal, false);
 
-        // We do not allow ExpandedPrincipals to contain any system principals.
-        bool isSystem;
-        rv = nsXPConnect::SecurityManager()->IsSystemPrincipal(principal, &isSystem);
-        NS_ENSURE_SUCCESS(rv, false);
-        if (isSystem) {
-            JS_ReportError(cx, "System principal is not allowed in an expanded principal");
+            if (!options.originAttributes) {
+                const PrincipalOriginAttributes prinAttrs =
+                    BasePrincipal::Cast(principal)->OriginAttributesRef();
+                if (attrs.isNothing()) {
+                    attrs.emplace(prinAttrs);
+                } else if (prinAttrs != attrs.ref()) {
+                    // If attrs is from a previously encountered principal in the
+                    // array, we need to ensure that it matches the OA of the
+                    // principal we have here.
+                    // If attrs comes from OriginAttributes, we don't need
+                    // this check.
+                    return false;
+                }
+            }
+
+            // We do not allow ExpandedPrincipals to contain any system principals.
+            bool isSystem;
+            rv = nsXPConnect::SecurityManager()->IsSystemPrincipal(principal, &isSystem);
+            NS_ENSURE_SUCCESS(rv, false);
+            if (isSystem) {
+                JS_ReportError(cx, "System principal is not allowed in an expanded principal");
+                return false;
+            }
+            allowedDomains[i] = principal;
+        } else if (allowed.isString()) {
+            // Skip any string arguments - we handle them in the next pass.
+        } else {
+            // Don't know what this is.
             return false;
         }
-        allowedDomains[i] = principal;
-  }
+    }
+
+    if (attrs.isNothing()) {
+        // If no OriginAttributes was found in the first pass, fall back to a default one.
+        attrs.emplace();
+    }
+
+    // Second pass:
+    for (uint32_t i = 0; i < length; ++i) {
+        RootedValue allowed(cx);
+        if (!JS_GetElement(cx, arrayObj, i, &allowed))
+            return false;
+
+        nsCOMPtr<nsIPrincipal> principal;
+        if (allowed.isString()) {
+            // In case of string let's try to fetch a codebase principal from it.
+            RootedString str(cx, allowed.toString());
 
-  nsCOMPtr<nsIExpandedPrincipal> result = new nsExpandedPrincipal(allowedDomains);
-  result.forget(out);
-  return true;
+            // attrs here is either a default OriginAttributes in case the
+            // originAttributes option isn't specified, and no object in the array
+            // provides a principal.  Otherwise it's either the forced principal, or
+            // the principal found before, so we can use it here.
+            if (!ParsePrincipal(cx, str, attrs.ref(), getter_AddRefs(principal)))
+                return false;
+            NS_ENSURE_TRUE(principal, false);
+            allowedDomains[i] = principal;
+        } else {
+            MOZ_ASSERT(allowed.isObject());
+        }
+    }
+
+    nsCOMPtr<nsIExpandedPrincipal> result =
+        new nsExpandedPrincipal(allowedDomains, attrs.ref());
+    result.forget(out);
+    return true;
 }
 
 /*
  * Helper that tries to get a property from the options object.
  */
 bool
 OptionsBase::ParseValue(const char* name, MutableHandleValue prop, bool* aFound)
 {
@@ -1598,17 +1670,18 @@ SandboxOptions::Parse()
               ParseObject("sameZoneAs", &sameZoneAs) &&
               ParseBoolean("freshZone", &freshZone) &&
               ParseBoolean("invisibleToDebugger", &invisibleToDebugger) &&
               ParseBoolean("discardSource", &discardSource) &&
               ParseJSString("addonId", &addonId) &&
               ParseBoolean("writeToGlobalPrototype", &writeToGlobalPrototype) &&
               ParseGlobalProperties() &&
               ParseValue("metadata", &metadata) &&
-              ParseUInt32("userContextId", &userContextId);
+              ParseUInt32("userContextId", &userContextId) &&
+              ParseObject("originAttributes", &originAttributes);
     if (!ok)
         return false;
 
     if (freshZone && sameZoneAs) {
         JS_ReportError(mCx, "Cannot use both sameZoneAs and freshZone");
         return false;
     }
 
@@ -1674,30 +1747,38 @@ nsXPCComponents_utils_Sandbox::CallOrCon
     // Make sure to set up principals on the sandbox before initing classes.
     nsCOMPtr<nsIPrincipal> principal;
     nsCOMPtr<nsIExpandedPrincipal> expanded;
     nsCOMPtr<nsISupports> prinOrSop;
 
     if (args[0].isString()) {
         RootedString str(cx, args[0].toString());
         PrincipalOriginAttributes attrs;
+        if (options.originAttributes) {
+            JS::RootedValue val(cx, JS::ObjectValue(*options.originAttributes));
+            if (!attrs.Init(cx, val)) {
+                // The originAttributes option, if specified, must be valid!
+                JS_ReportError(cx, "Expected a valid OriginAttributes object");
+                return ThrowAndFail(NS_ERROR_INVALID_ARG, cx, _retval);
+            }
+        }
         attrs.mUserContextId = options.userContextId;
         ok = ParsePrincipal(cx, str, attrs, getter_AddRefs(principal));
         prinOrSop = principal;
     } else if (args[0].isObject()) {
         RootedObject obj(cx, &args[0].toObject());
         bool isArray;
         if (!JS_IsArrayObject(cx, obj, &isArray)) {
             ok = false;
         } else if (isArray) {
             if (options.userContextId != 0) {
                 // We don't support passing a userContextId with an array.
                 ok = false;
             } else {
-                ok = GetExpandedPrincipal(cx, obj, getter_AddRefs(expanded));
+                ok = GetExpandedPrincipal(cx, obj, options, getter_AddRefs(expanded));
                 prinOrSop = expanded;
             }
         } else {
             ok = GetPrincipalOrSOP(cx, obj, getter_AddRefs(prinOrSop));
         }
     } else if (args[0].isNull()) {
         // Null means that we just pass prinOrSop = nullptr, and get an
         // nsNullPrincipal.
--- a/js/xpconnect/src/xpcprivate.h
+++ b/js/xpconnect/src/xpcprivate.h
@@ -3206,16 +3206,17 @@ public:
         , addonId(cx)
         , writeToGlobalPrototype(false)
         , sameZoneAs(cx)
         , freshZone(false)
         , invisibleToDebugger(false)
         , discardSource(false)
         , metadata(cx)
         , userContextId(0)
+        , originAttributes(cx)
     { }
 
     virtual bool Parse();
 
     bool wantXrays;
     bool allowWaivers;
     bool wantComponents;
     bool wantExportHelpers;
@@ -3227,16 +3228,17 @@ public:
     bool writeToGlobalPrototype;
     JS::RootedObject sameZoneAs;
     bool freshZone;
     bool invisibleToDebugger;
     bool discardSource;
     GlobalProperties globalProperties;
     JS::RootedValue metadata;
     uint32_t userContextId;
+    JS::RootedObject originAttributes;
 
 protected:
     bool ParseGlobalProperties();
 };
 
 class MOZ_STACK_CLASS CreateObjectInOptions : public OptionsBase {
 public:
     explicit CreateObjectInOptions(JSContext* cx = xpc_GetSafeJSContext(),
--- a/toolkit/components/extensions/ExtensionContent.jsm
+++ b/toolkit/components/extensions/ExtensionContent.jsm
@@ -373,16 +373,17 @@ class ExtensionContext extends BaseConte
 
       this.sandbox = Cu.Sandbox(prin, {
         metadata,
         sandboxPrototype: contentWindow,
         wantXrays: true,
         isWebExtensionContentScript: true,
         wantExportHelpers: true,
         wantGlobalProperties: ["XMLHttpRequest", "fetch"],
+        originAttributes: attrs,
       });
 
       Cu.evalInSandbox(`
         window.JSON = JSON;
         window.XMLHttpRequest = XMLHttpRequest;
         window.fetch = fetch;
       `, this.sandbox);
     }