Bug 1277876 - Add a mechanism which disallows plugins from a list of domains, when those domains are loaded in a 3rd-party context. r?bz draft
authorBenjamin Smedberg <benjamin@smedbergs.us>
Thu, 07 Jul 2016 13:52:19 -0400
changeset 394140 803c4619fe3fc4a1dd6fbf8e860b6b24bb660f88
parent 393861 9ec789c0ee5bd3a5e765513c21027fdad953b022
child 526741 bcea7975838737fdfee6aa18fd9cdedf0a0d9fad
push id24499
push userfelipc@gmail.com
push dateFri, 29 Jul 2016 04:04:42 +0000
reviewersbz
bugs1277876
milestone50.0a1
Bug 1277876 - Add a mechanism which disallows plugins from a list of domains, when those domains are loaded in a 3rd-party context. r?bz MozReview-Commit-ID: I4TVPpYluK7
docshell/base/nsDocShell.cpp
docshell/base/nsDocShell.h
docshell/base/nsIDocShell.idl
dom/base/nsDocument.cpp
dom/base/nsDocument.h
dom/plugins/test/mochitest/file_thirdparty.html
dom/plugins/test/mochitest/file_thirdparty_top.html
dom/plugins/test/mochitest/mochitest.ini
dom/plugins/test/mochitest/test_thirdparty.html
--- a/docshell/base/nsDocShell.cpp
+++ b/docshell/base/nsDocShell.cpp
@@ -6256,27 +6256,45 @@ nsDocShell::GetIsAppTab(bool* aIsAppTab)
   *aIsAppTab = mIsAppTab;
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsDocShell::SetSandboxFlags(uint32_t aSandboxFlags)
 {
   mSandboxFlags = aSandboxFlags;
+  if (mSandboxPlugins) {
+    mSandboxFlags |= SANDBOXED_PLUGINS;
+  }
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsDocShell::GetSandboxFlags(uint32_t* aSandboxFlags)
 {
   *aSandboxFlags = mSandboxFlags;
   return NS_OK;
 }
 
 NS_IMETHODIMP
+nsDocShell::SandboxPlugins()
+{
+  mSandboxPlugins = true;
+  mSandboxFlags |= SANDBOXED_PLUGINS;
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+nsDocShell::ArePluginsSandboxed(bool* aResult)
+{
+  *aResult = mSandboxPlugins;
+  return NS_OK;
+}
+
+NS_IMETHODIMP
 nsDocShell::SetOnePermittedSandboxedNavigator(nsIDocShell* aSandboxedNavigator)
 {
   if (mOnePermittedSandboxedNavigator) {
     NS_ERROR("One Permitted Sandboxed Navigator should only be set once.");
     return NS_OK;
   }
 
   mOnePermittedSandboxedNavigator = do_GetWeakReference(aSandboxedNavigator);
--- a/docshell/base/nsDocShell.h
+++ b/docshell/base/nsDocShell.h
@@ -924,16 +924,17 @@ protected:
   FullscreenAllowedState mFullscreenAllowed;
 
   // Cached value of the "browser.xul.error_pages.enabled" preference.
   static bool sUseErrorPages;
 
   bool mCreated : 1;
   bool mAllowSubframes : 1;
   bool mAllowPlugins : 1;
+  bool mSandboxPlugins : 1;
   bool mAllowJavascript : 1;
   bool mAllowMetaRedirects : 1;
   bool mAllowImages : 1;
   bool mAllowMedia : 1;
   bool mAllowDNSPrefetch : 1;
   bool mAllowWindowControl : 1;
   bool mAllowContentRetargeting : 1;
   bool mAllowContentRetargetingOnChildren : 1;
--- a/docshell/base/nsIDocShell.idl
+++ b/docshell/base/nsIDocShell.idl
@@ -882,16 +882,26 @@ interface nsIDocShell : nsIDocShellTreeI
    * loaded.
    * The sandbox flags of a document depend on the sandbox flags on its
    * docshell and of its parent document, if any.
    * See nsSandboxFlags.h for the possible flags.
    */
   attribute unsigned long sandboxFlags;
 
   /**
+   * When some 3rd-party content is loaded, we sandbox plugins from
+   * that content in the current page and future loads in that iframe.
+   * This flag is combined with the normal sandbox flags that come from
+   * content. The effects of this show up in the sandbox flags, not in the
+   * `allowPlugins` attribute.
+   */
+  void sandboxPlugins();
+  bool arePluginsSandboxed();
+
+  /**
    * When a new browsing context is opened by a sandboxed document, it needs to
    * keep track of the browsing context that opened it, so that it can be
    * navigated by it.  This is the "one permitted sandboxed navigator".
    */
   attribute nsIDocShell onePermittedSandboxedNavigator;
 
   /**
    * Returns true if we are sandboxed from aTargetDocShell.
--- a/dom/base/nsDocument.cpp
+++ b/dom/base/nsDocument.cpp
@@ -255,16 +255,18 @@
 
 #include "nsISpeculativeConnect.h"
 
 #include "mozilla/MediaManager.h"
 #ifdef MOZ_WEBRTC
 #include "IPeerConnection.h"
 #endif // MOZ_WEBRTC
 
+#include "mozIThirdPartyUtil.h"
+
 using namespace mozilla;
 using namespace mozilla::dom;
 
 typedef nsTArray<Link*> LinkArray;
 
 static LazyLogModule gDocumentLeakPRLog("DocumentLeak");
 static LazyLogModule gCspPRLog("CSP");
 
@@ -2433,16 +2435,100 @@ nsDocument::FillStyleSet(StyleSetHandle 
   AppendSheetsToStyleSet(aStyleSet, mAdditionalSheets[eUserSheet],
                          SheetType::User);
   AppendSheetsToStyleSet(aStyleSet, mAdditionalSheets[eAuthorSheet],
                          SheetType::Doc);
 
   mStyleSetFilled = true;
 }
 
+static nsTArray<nsCString>
+GetPluginBlockDomainList()
+{
+  // For the moment, we're keeping this list in a pref, comma-delimited.
+  // Future work: load this dynamically from Kinto.
+  nsTArray<nsCString> domains;
+  ParseString(Preferences::GetCString("dom.plugins.block_third_party_domains"), ',', domains);
+  return domains;
+}
+
+// returns true if 'a' is equal to or a subdomain of 'b',
+// assuming no leading dots are present.
+// copied from nsCookieService.cpp
+static inline bool
+IsSubdomainOf(const nsCString &a, const nsCString &b)
+{
+  if (a == b)
+    return true;
+  if (a.Length() > b.Length())
+    return a[a.Length() - b.Length() - 1] == '.' && StringEndsWith(a, b);
+  return false;
+}
+
+void
+nsDocument::UpdatePluginSandbox(nsIDocShell* aDocShell, nsIChannel* aChannel)
+{
+  bool sandboxed = false;
+  aDocShell->ArePluginsSandboxed(&sandboxed);
+  if (sandboxed) {
+    return;
+  }
+
+  nsCOMPtr<nsIDocShellTreeItem> parentAsItem;
+  aDocShell->GetSameTypeParent(getter_AddRefs(parentAsItem));
+  nsCOMPtr<nsIWebNavigation> parentAsNav(do_QueryInterface(parentAsItem));
+
+  if (!parentAsNav) {
+    return;
+  }
+
+  nsCOMPtr<nsIURI> parentURI;
+  nsresult rv = parentAsNav->GetCurrentURI(getter_AddRefs(parentURI));
+  NS_ENSURE_SUCCESS_VOID(rv);
+
+  nsCOMPtr<mozIThirdPartyUtil> thirdPartyUtil =
+    do_GetService(THIRDPARTYUTIL_CONTRACTID);
+  if (!thirdPartyUtil) {
+    return;
+  }
+
+  bool isThirdPartyURI = false;
+  rv = thirdPartyUtil->IsThirdPartyURI(mOriginalURI, parentURI,
+                                       &isThirdPartyURI);
+
+  // The plugin sandbox only applies to 3rd-party documents
+  if (!isThirdPartyURI) {
+    return;
+  }
+
+  // We only check HTTP/HTTPS
+  nsAutoCString scheme;
+  mOriginalURI->GetScheme(scheme);
+  if (scheme != "http" && scheme != "https") {
+    return;
+  }
+
+  nsAutoCString domain;
+  mOriginalURI->GetHost(domain);
+
+  // Is this document loading an origin on the blacklist?
+  for (auto blockDomain : GetPluginBlockDomainList()) {
+    if (blockDomain.IsEmpty()) {
+      continue;
+    }
+    if (IsSubdomainOf(domain, blockDomain)) {
+      mSandboxFlags |= SANDBOXED_PLUGINS;
+      // Propagate the plugin sandbox flags back up to the docshell so that
+      // future content loaded in this iframes is also plugin-sandboxed
+      aDocShell->SandboxPlugins();
+      return;
+    }
+  }
+}
+
 static void
 WarnIfSandboxIneffective(nsIDocShell* aDocShell,
                          uint32_t aSandboxFlags,
                          nsIChannel* aChannel)
 {
   // If the document is sandboxed (via the HTML5 iframe sandbox
   // attribute) and both the allow-scripts and allow-same-origin
   // keywords are supplied, the sandboxed document can call into its
@@ -2561,16 +2647,19 @@ nsDocument::StartDocumentLoad(const char
   // If this document is being loaded by a docshell, copy its sandbox flags
   // to the document, and store the fullscreen enabled flag. These are
   // immutable after being set here.
   nsCOMPtr<nsIDocShell> docShell = do_QueryInterface(aContainer);
 
   if (docShell) {
     nsresult rv = docShell->GetSandboxFlags(&mSandboxFlags);
     NS_ENSURE_SUCCESS(rv, rv);
+
+    UpdatePluginSandbox(docShell, GetChannel());
+
     WarnIfSandboxIneffective(docShell, mSandboxFlags, GetChannel());
   }
 
   // The CSP directive upgrade-insecure-requests not only applies to the
   // toplevel document, but also to nested documents. Let's propagate that
   // flag from the parent to the nested document.
   nsCOMPtr<nsIDocShellTreeItem> treeItem = this->GetDocShell();
   if (treeItem) {
--- a/dom/base/nsDocument.h
+++ b/dom/base/nsDocument.h
@@ -616,16 +616,18 @@ public:
   NS_DECL_CYCLE_COLLECTING_ISUPPORTS
 
   NS_DECL_SIZEOF_EXCLUDING_THIS
 
   virtual void Reset(nsIChannel *aChannel, nsILoadGroup *aLoadGroup) override;
   virtual void ResetToURI(nsIURI *aURI, nsILoadGroup *aLoadGroup,
                           nsIPrincipal* aPrincipal) override;
 
+  void UpdatePluginSandbox(nsIDocShell* aDocShell, nsIChannel* aChannel);
+
   // StartDocumentLoad is pure virtual so that subclasses must override it.
   // The nsDocument StartDocumentLoad does some setup, but does NOT set
   // *aDocListener; this is the job of subclasses.
   virtual nsresult StartDocumentLoad(const char* aCommand,
                                      nsIChannel* aChannel,
                                      nsILoadGroup* aLoadGroup,
                                      nsISupports* aContainer,
                                      nsIStreamListener **aDocListener,
new file mode 100644
--- /dev/null
+++ b/dom/plugins/test/mochitest/file_thirdparty.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8">
+    <title>Page that loads a plugin</title>
+  </head>
+  <body>
+    <h1>Page that loads a plugin</h1>
+    <object id="p" type="application/x-test" width="200" height="200">
+      <div id="fallback">Fallback content</div>
+    </object>
+
+    <p>navigator.mimeTypes reports the test plugin: <span id="navigator"></span>
+    <script type="text/javascript">
+      document.getElementById("navigator").textContent = "application/x-test" in navigator.mimeTypes;
+    </script>
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/plugins/test/mochitest/file_thirdparty_top.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8">
+    <title>Page that loads a plugin and a same-origin subframe</title>
+  </head>
+  <body>
+    <h1>Page that loads a plugin and a same-origin subframe</h1>
+    <object id="p" type="application/x-test" width="200" height="200">
+      <div id="fallback">Fallback content</div>
+    </object>
+
+    <p>navigator.mimeTypes reports the test plugin: <span id="navigator"></span>
+    <script type="text/javascript">
+      document.getElementById("navigator").textContent = "application/x-test" in navigator.mimeTypes;
+    </script>
+
+    <p><iframe id="f" src="file_thirdparty.html"></iframe>
+    <script type="text/javascript">
+      window.addEventListener("load", function() {
+        if (window.opener) {
+          window.opener.postMessage("thirdparty-loaded", "http://mochi.test:8888");
+        }
+      });
+    </script>
+  </body>
+</html>
--- a/dom/plugins/test/mochitest/mochitest.ini
+++ b/dom/plugins/test/mochitest/mochitest.ini
@@ -125,16 +125,20 @@ skip-if = true # disabled due to oddness
 [test_queryContentsScaleFactor.html]
 skip-if = toolkit != "cocoa"
 [test_redirect_handling.html]
 [test_secondPlugin.html]
 [test_src_url_change.html]
 [test_streamatclose.html]
 [test_streamNotify.html]
 [test_stringHandling.html]
+[test_thirdparty.html]
+support-files =
+  file_thirdparty.html
+  file_thirdparty_top.html
 [test_twostreams.html]
 [test_visibility.html]
 skip-if = toolkit == "cocoa"
 [test_windowed_invalidate.html]
 skip-if = os != "win"
 [test_windowless_flash.html]
 skip-if = !(os == "win" && processor == "x86_64")
 [test_windowless_ime.html]
new file mode 100644
--- /dev/null
+++ b/dom/plugins/test/mochitest/test_thirdparty.html
@@ -0,0 +1,137 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8">
+    <title>Blocking 3rd-party plugins</title>
+
+    <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+  </head>
+  <body onload="start()">
+
+    <p id="display"></p>
+
+    <p id="frame-container"></p>
+
+    <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+    <script type="application/javascript" src="/tests/SimpleTest/SpecialPowers.js"></script>
+    <script type="text/javascript" src="plugin-utils.js"></script>
+    <script class="testbody" type="text/javascript">
+      SimpleTest.waitForExplicitFinish();
+      setTestPluginEnabledState(SpecialPowers.Ci.nsIPluginTag.STATE_ENABLED);
+
+      function url_for_domain(domain, protocol="http", top=false) {
+        let fname = top ? "file_thirdparty_top.html" : "file_thirdparty.html";
+        return protocol + "://" + domain + "/tests/dom/plugins/test/mochitest/" + fname;
+      }
+
+      function waitForLoad(node) {
+        return new Promise((resolve, reject) => {
+          function onLoad(e) {
+            if (e.target === e.originalTarget) {
+              node.removeEventListener("load", onLoad);
+              resolve();
+            }
+          }
+          node.addEventListener("load", onLoad);
+        });
+      }
+
+      function waitForLoadMessage(w) {
+        return new Promise((resolve, reject) => {
+          function handler(e) {
+            if (e.source === w) {
+              window.removeEventListener("message", handler);
+              resolve();
+            }
+          }
+          window.addEventListener("message", handler);
+        });
+      }
+
+      var gFrame;
+
+      // Each test needs to be loaded in a *different* frame, because this
+      // feature intentionally sandboxes the frame across loads
+      function loadInFrame(url) {
+        let container = document.getElementById("frame-container");
+        container.textContent = '';
+        gFrame = document.createElement("iframe");
+        gFrame.width = 400;
+        gFrame.height = 400;
+        container.appendChild(gFrame);
+        let p = waitForLoad(gFrame);
+        gFrame.src = url;
+        return p;
+      };
+
+      function check_plugins_enabled(window, enabled, info) {
+        var doc = SpecialPowers.wrap(window).document;
+        var plugin = doc.getElementById("p");
+        var fallback = doc.getElementById("fallback");
+        var navigator = doc.getElementById("navigator");
+
+        if (enabled) {
+          ok(SpecialPowers.do_QueryInterface(plugin, "nsIObjectLoadingContent").activated,
+             info + ": plugin is activated");
+          is(fallback.getBoundingClientRect().width, 0, info + ": fallback is invisible");
+          is(navigator.textContent, "true", info + ": navigator.mimeTypes has test plugin");
+        } else {
+          ok(!SpecialPowers.do_QueryInterface(plugin, "nsIObjectLoadingContent").activated,
+             info + ": plugin is not activated");
+          isnot(fallback.getBoundingClientRect().width, 0, info + ": fallback is visible");
+          is(navigator.textContent, "false", info + ": navigator.mimeTypes doesn't have test plugin");
+        }
+      }
+
+      function start() {
+        the_tests().then(
+          () => { SimpleTest.finish(); },
+          (err) => {
+            console.error(err, err.stack);
+            ok(false, "Error caught during test execution: " + err + ": " + err.stack);
+            SimpleTest.finish();
+          });
+      }
+
+      function the_tests() {
+        let openedWindow;
+
+        return loadInFrame(url_for_domain("example.com"))
+        .then(() => {
+          check_plugins_enabled(gFrame.contentWindow, true, "example.com should not initially be blocked");
+        }).then(() => {
+          return SpecialPowers.pushPrefEnv({set: [['dom.plugins.block_third_party_domains', 'example.com']]});
+        }).then(() => {
+          return loadInFrame(url_for_domain("mochi.test:8888"));
+        }).then(() => {
+          check_plugins_enabled(gFrame.contentWindow, true, "mochi.test:8888 should not be blocked");
+
+          return loadInFrame(url_for_domain("example.com"));
+        }).then(() => {
+          check_plugins_enabled(gFrame.contentWindow, false, "example.com should now be blocked");
+
+          openedWindow = window.open(url_for_domain("example.com", "http", true), "_blank");
+          return waitForLoadMessage(openedWindow);
+        }).then(() => {
+          check_plugins_enabled(openedWindow, true, "plugins enabled in first-party window");
+           let subf = SpecialPowers.wrap(openedWindow).document
+             .getElementById("f");
+          check_plugins_enabled(subf.contentWindow, true, "plugins enabled in same-origin frame on blocklist");
+          openedWindow.close();
+          return loadInFrame(url_for_domain("test1.example.com"));
+        }).then(() => {
+          check_plugins_enabled(gFrame.contentWindow, false, "test1.example.com should be blocked");
+
+          return loadInFrame(url_for_domain("example.com"), "https");
+        }).then(() => {
+          check_plugins_enabled(gFrame.contentWindow, false, "blocking applies to HTTPS also");
+
+          let p = waitForLoad(gFrame);
+          gFrame.src = url_for_domain("mochi.test:8888");
+        }).then(() => {
+          check_plugins_enabled(gFrame.contentWindow, false, "blocking should continue across frame navigation");
+        });
+      }
+    </script>
+  </body>
+</html>