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 252516 45c0c49d87c847727ba2c99ecd56332579480d91
parent 252515 fea515a8f58fd1e03b7b143056be7dd39807a227
child 252517 b14ae415db55399e8b5f4d0af80361be55bc66f0
push id13977
push usercbook@mozilla.com
push dateMon, 13 Jul 2015 12:54:14 +0000
treeherderfx-team@149e770b65ac [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbz, billm
bugs1180921
milestone42.0a1
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}