Bug 1180921 - Support custom callbacks for allowing access per-addon load access to cross-origin URIs. r=bz,r=billm
authorBobby Holley <bobbyholley@gmail.com>
Tue, 07 Jul 2015 17:53:15 -0700
changeset 252468 45c0c49d87c847727ba2c99ecd56332579480d91
parent 252467 fea515a8f58fd1e03b7b143056be7dd39807a227
child 252469 b14ae415db55399e8b5f4d0af80361be55bc66f0
push id62156
push userbobbyholley@gmail.com
push dateSat, 11 Jul 2015 14:34:03 +0000
treeherdermozilla-inbound@45c0c49d87c8 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbz, billm
bugs1180921
milestone42.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1180921 - Support custom callbacks for allowing access per-addon load access to cross-origin URIs. r=bz,r=billm
caps/BasePrincipal.cpp
caps/BasePrincipal.h
caps/moz.build
caps/nsIAddonPolicyService.idl
caps/nsPrincipal.cpp
caps/tests/mochitest/chrome.ini
caps/tests/mochitest/file_data.txt
caps/tests/mochitest/mochitest.ini
caps/tests/mochitest/test_addonMayLoad.html
toolkit/components/utils/simpleServices.js
toolkit/components/utils/utils.manifest
--- a/caps/BasePrincipal.cpp
+++ b/caps/BasePrincipal.cpp
@@ -1,25 +1,27 @@
 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim: set ts=2 sw=2 et tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "mozilla/BasePrincipal.h"
 
+#include "nsIAddonPolicyService.h"
 #include "nsIContentSecurityPolicy.h"
 #include "nsIObjectInputStream.h"
 #include "nsIObjectOutputStream.h"
 
 #include "nsPrincipal.h"
 #include "nsNetUtil.h"
 #include "nsIURIWithPrincipal.h"
 #include "nsNullPrincipal.h"
 #include "nsScriptSecurityManager.h"
+#include "nsServiceManagerUtils.h"
 
 #include "mozilla/dom/CSPDictionariesBinding.h"
 #include "mozilla/dom/ToJSValue.h"
 #include "mozilla/dom/URLSearchParams.h"
 
 namespace mozilla {
 
 using dom::URLParams;
@@ -351,9 +353,24 @@ BasePrincipal::CreateCodebasePrincipal(n
 
   // Mint a codebase principal.
   nsRefPtr<nsPrincipal> codebase = new nsPrincipal();
   rv = codebase->Init(aURI, aAttrs);
   NS_ENSURE_SUCCESS(rv, nullptr);
   return codebase.forget();
 }
 
+bool
+BasePrincipal::AddonAllowsLoad(nsIURI* aURI)
+{
+  if (mOriginAttributes.mAddonId.IsEmpty()) {
+    return false;
+  }
+
+  nsCOMPtr<nsIAddonPolicyService> aps = do_GetService("@mozilla.org/addons/policy-service;1");
+  NS_ENSURE_TRUE(aps, false);
+
+  bool allowed = false;
+  nsresult rv = aps->AddonMayLoadURI(mOriginAttributes.mAddonId, aURI, &allowed);
+  return NS_SUCCEEDED(rv) && allowed;
+}
+
 } // namespace mozilla
--- a/caps/BasePrincipal.h
+++ b/caps/BasePrincipal.h
@@ -102,15 +102,19 @@ public:
   bool IsInBrowserElement() const { return mOriginAttributes.mInBrowser; }
 
 protected:
   virtual ~BasePrincipal();
 
   virtual nsresult GetOriginInternal(nsACString& aOrigin) = 0;
   virtual bool SubsumesInternal(nsIPrincipal* aOther, DocumentDomainConsideration aConsider) = 0;
 
+  // Helper to check whether this principal is associated with an addon that
+  // allows unprivileged code to load aURI.
+  bool AddonAllowsLoad(nsIURI* aURI);
+
   nsCOMPtr<nsIContentSecurityPolicy> mCSP;
   OriginAttributes mOriginAttributes;
 };
 
 } // namespace mozilla
 
 #endif /* mozilla_BasePrincipal_h */
--- a/caps/moz.build
+++ b/caps/moz.build
@@ -4,16 +4,17 @@
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 MOCHITEST_MANIFESTS += ['tests/mochitest/mochitest.ini']
 MOCHITEST_CHROME_MANIFESTS += ['tests/mochitest/chrome.ini']
 XPCSHELL_TESTS_MANIFESTS += ['tests/unit/xpcshell.ini']
 
 XPIDL_SOURCES += [
+    'nsIAddonPolicyService.idl',
     'nsIDomainPolicy.idl',
     'nsIPrincipal.idl',
     'nsIScriptSecurityManager.idl',
 ]
 
 XPIDL_MODULE = 'caps'
 
 EXPORTS += [
new file mode 100644
--- /dev/null
+++ b/caps/nsIAddonPolicyService.idl
@@ -0,0 +1,22 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+#include "nsIURI.idl"
+
+/**
+ * This interface allows the security manager to query custom per-addon security
+ * policy.
+ */
+[scriptable,uuid(fedf126c-988e-42df-82c9-f2ac99cd65f3)]
+interface nsIAddonPolicyService : nsISupports
+{
+  /**
+   * Returns true if unprivileged code associated with the given addon may load
+   * data from |aURI|.
+   */
+  boolean addonMayLoadURI(in AString aAddonId, in nsIURI aURI);
+};
--- a/caps/nsPrincipal.cpp
+++ b/caps/nsPrincipal.cpp
@@ -231,16 +231,22 @@ nsPrincipal::CheckMayLoad(nsIURI* aURI, 
   nsCOMPtr<nsIPrincipal> uriPrin;
   if (uriWithPrin) {
     uriWithPrin->GetPrincipal(getter_AddRefs(uriPrin));
   }
   if (uriPrin && nsIPrincipal::Subsumes(uriPrin)) {
       return NS_OK;
   }
 
+  // If this principal is associated with an addon, check whether that addon
+  // has been given permission to load from this domain.
+  if (AddonAllowsLoad(aURI)) {
+    return NS_OK;
+  }
+
   if (nsScriptSecurityManager::SecurityCompareURIs(mCodebase, aURI)) {
     return NS_OK;
   }
 
   // If strict file origin policy is in effect, local files will always fail
   // SecurityCompareURIs unless they are identical. Explicitly check file origin
   // policy, in that case.
   if (nsScriptSecurityManager::GetStrictFileOriginPolicy() &&
--- a/caps/tests/mochitest/chrome.ini
+++ b/caps/tests/mochitest/chrome.ini
@@ -1,10 +1,11 @@
 [DEFAULT]
 skip-if = buildapp == 'b2g' || os == 'android'
 support-files =
   file_disableScript.html
 
 [test_bug995943.xul]
+[test_addonMayLoad.html]
 [test_disableScript.xul]
 [test_principal_jarprefix_origin_appid_appstatus.html]
 # jarPrefix test doesn't work on Windows, see bug 776296.
 skip-if = os == "win"
new file mode 100644
--- /dev/null
+++ b/caps/tests/mochitest/file_data.txt
@@ -0,0 +1,1 @@
+server data fetched over XHR
--- a/caps/tests/mochitest/mochitest.ini
+++ b/caps/tests/mochitest/mochitest.ini
@@ -1,10 +1,11 @@
 [DEFAULT]
 support-files =
+  file_data.txt
   file_disableScript.html
 
 [test_app_principal_equality.html]
 skip-if = e10s
 [test_bug246699.html]
 [test_bug292789.html]
 skip-if = (buildapp == 'b2g' && toolkit != 'gonk') #Bug 931116, b2g desktop specific, initial triage
 [test_bug423375.html]
new file mode 100644
--- /dev/null
+++ b/caps/tests/mochitest/test_addonMayLoad.html
@@ -0,0 +1,97 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1180921
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Test for Bug 1180921</title>
+  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="chrome://global/skin"/>
+  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+  <script type="application/javascript;version=1.8">
+
+  /** Test for Bug 1180921 **/
+  const Cc = Components.classes;
+  const Ci = Components.interfaces;
+  const Cu = Components.utils;
+  Cu.import("resource://gre/modules/Services.jsm");
+  let ssm = Services.scriptSecurityManager;
+  let aps = Cc["@mozilla.org/addons/policy-service;1"].getService(Ci.nsIAddonPolicyService).wrappedJSObject;
+
+  SimpleTest.waitForExplicitFinish();
+  SimpleTest.registerCleanupFunction(function() {
+    aps.setAddonLoadURICallback('addonA', null);
+    aps.setAddonLoadURICallback('addonB', null);
+  });
+
+  function tryLoad(sb, uri) {
+    let p = new Promise(function(resolve, reject) {
+      Cu.exportFunction(resolve, sb, { defineAs: "finish" });
+      Cu.exportFunction(reject, sb, { defineAs: "error" });
+      sb.eval("try { (function () { " +
+              "  var xhr = new XMLHttpRequest();" +
+              "  xhr.onreadystatechange = function() { if (xhr.readyState == XMLHttpRequest.DONE) { finish(xhr.status == 200); } };" +
+              "  xhr.open('GET', '" + uri + "', true);" +
+              "  xhr.send();" +
+              "})() } catch (e) { error(e); }");
+    });
+    return p;
+  }
+
+  let exampleCom_addonA = new Cu.Sandbox(ssm.createCodebasePrincipal(Services.io.newURI('http://example.com', null, null), {addonId: 'addonA'}),
+                                         {wantGlobalProperties: ['XMLHttpRequest']});
+  let nullPrin_addonA = new Cu.Sandbox(ssm.createNullPrincipal({addonId: 'addonA'}),
+                                       {wantGlobalProperties: ['XMLHttpRequest']});
+  let exampleCom_addonB = new Cu.Sandbox(ssm.createCodebasePrincipal(Services.io.newURI('http://example.com', null, null), {addonId: 'addonB'}),
+                                         {wantGlobalProperties: ['XMLHttpRequest']});
+
+  function uriForDomain(d) { return d + '/tests/caps/tests/mochitest/file_data.txt' }
+
+  tryLoad(exampleCom_addonA, uriForDomain('http://example.com'))
+  .then(function(success) {
+    ok(success, "same-origin load should succeed for addon A");
+    return tryLoad(nullPrin_addonA, uriForDomain('http://example.com'));
+  }).then(function(success) {
+    ok(!success, "null-principal load should fail for addon A");
+    return tryLoad(exampleCom_addonB, uriForDomain('http://example.com'));
+  }).then(function(success) {
+    ok(success, "same-origin load should succeed for addon B");
+    return tryLoad(exampleCom_addonA, uriForDomain('http://test1.example.org'));
+  }).then(function(success) {
+    ok(!success, "cross-origin load should fail for addon A");
+    aps.setAddonLoadURICallback('addonA', function(uri) { return /test1/.test(uri.host); });
+    aps.setAddonLoadURICallback('addonB', function(uri) { return /test2/.test(uri.host); });
+    return tryLoad(exampleCom_addonA, uriForDomain('http://test1.example.org'));
+  }).then(function(success) {
+    ok(success, "whitelisted cross-origin load of test1 should succeed for addon A");
+    return tryLoad(nullPrin_addonA, uriForDomain('http://test1.example.org'));
+  }).then(function(success) {
+    ok(!success, "whitelisted null principal load of test1 should still fail for addon A");
+    return tryLoad(exampleCom_addonB, uriForDomain('http://test1.example.org'));
+  }).then(function(success) {
+    ok(!success, "non-whitelisted cross-origin load of test1 should fail for addon B");
+    return tryLoad(exampleCom_addonB, uriForDomain('http://test2.example.org'));
+  }).then(function(success) {
+    ok(success, "whitelisted cross-origin load of test2 should succeed for addon B");
+    return tryLoad(exampleCom_addonA, uriForDomain('http://test2.example.org'));
+  }).then(function(success) {
+    ok(!success, "non-whitelisted cross-origin load of test2 should fail for addon A");
+    SimpleTest.finish();
+  }, function(e) {
+    ok(false, "Rejected promise chain: " + e);
+    SimpleTest.finish();
+  });
+
+  </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1180921">Mozilla Bug 1180921</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
--- a/toolkit/components/utils/simpleServices.js
+++ b/toolkit/components/utils/simpleServices.js
@@ -39,9 +39,41 @@ RemoteTagServiceService.prototype = {
     if (target instanceof Ci.nsIDOMDocument) {
       return "ContentDocument";
     }
 
     return "generic";
   }
 };
 
-this.NSGetFactory = XPCOMUtils.generateNSGetFactory([RemoteTagServiceService]);
+function AddonPolicyService()
+{
+  this.wrappedJSObject = this;
+  this.mayLoadURICallbacks = new Map();
+}
+
+AddonPolicyService.prototype = {
+  classID: Components.ID("{89560ed3-72e3-498d-a0e8-ffe50334d7c5}"),
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIAddonPolicyService]),
+
+  /*
+   * Invokes a callback (if any) associated with the addon to determine whether
+   * unprivileged code running within the addon is allowed to perform loads from
+   * the given URI.
+   *
+   * @see nsIAddonPolicyService.addonMayLoadURI
+   */
+  addonMayLoadURI(aAddonId, aURI) {
+    let cb = this.mayLoadURICallbacks[aAddonId];
+    return cb ? cb(aURI) : false;
+  },
+
+  /*
+   * Sets the callbacks used in addonMayLoadURI above. Not accessible over
+   * XPCOM - callers should use .wrappedJSObject on the service to call it
+   * directly.
+   */
+  setAddonLoadURICallback(aAddonId, aCallback) {
+    this.mayLoadURICallbacks[aAddonId] = aCallback;
+  },
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([RemoteTagServiceService, AddonPolicyService]);
--- a/toolkit/components/utils/utils.manifest
+++ b/toolkit/components/utils/utils.manifest
@@ -1,2 +1,4 @@
 component {dfd07380-6083-11e4-9803-0800200c9a66} simpleServices.js
 contract @mozilla.org/addons/remote-tag-service;1 {dfd07380-6083-11e4-9803-0800200c9a66}
+component {89560ed3-72e3-498d-a0e8-ffe50334d7c5} simpleServices.js
+contract @mozilla.org/addons/policy-service;1 {89560ed3-72e3-498d-a0e8-ffe50334d7c5}