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
--- 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>