Bug 1514547 - Timing token to allow external protocol URLs are blocked in iframes without user-interaction, r=smaug
authorAndrea Marchesini <amarchesini@mozilla.com>
Thu, 24 Jan 2019 20:05:03 +0100
changeset 515368 910e176da1f9597774356dacb6bca91cb68935d4
parent 515367 eac3071c276bbab564bfaa5f8c2df23ae3f3e453
child 515369 da2d6417cfda1e9d9cbe5a02bd5829e02a71ceb9
push id1953
push userffxbld-merge
push dateMon, 11 Mar 2019 12:10:20 +0000
treeherdermozilla-release@9c35dcbaa899 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssmaug
bugs1514547
milestone66.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 1514547 - Timing token to allow external protocol URLs are blocked in iframes without user-interaction, r=smaug
docshell/base/nsDocShell.cpp
dom/base/ChromeUtils.cpp
dom/base/ChromeUtils.h
dom/base/PopupBlocker.cpp
dom/base/PopupBlocker.h
dom/chrome-webidl/ChromeUtils.webidl
dom/html/test/test_external_protocol_iframe.html
modules/libpref/init/StaticPrefList.h
--- a/docshell/base/nsDocShell.cpp
+++ b/docshell/base/nsDocShell.cpp
@@ -9649,16 +9649,19 @@ nsresult nsDocShell::DoURILoad(nsDocShel
       if (doesNotReturnData) {
         bool popupBlocked = true;
 
         // Let's consider external protocols as popups and let's check if the
         // page is allowed to open them without abuse regardless of allowed
         // events
         if (PopupBlocker::GetPopupControlState() <= PopupBlocker::openBlocked) {
           popupBlocked = !PopupBlocker::TryUsePopupOpeningToken();
+        } else if (mIsActive &&
+                   PopupBlocker::ConsumeTimerTokenForExternalProtocolIframe()) {
+          popupBlocked = false;
         } else {
           nsCOMPtr<nsINode> loadingNode =
               mScriptGlobal->AsOuter()->GetFrameElementInternal();
           if (loadingNode) {
             popupBlocked = !PopupBlocker::CanShowPopupByPermission(
                 loadingNode->NodePrincipal());
           }
         }
--- a/dom/base/ChromeUtils.cpp
+++ b/dom/base/ChromeUtils.cpp
@@ -795,16 +795,27 @@ constexpr auto kSkipSelfHosted = JS::Sav
           "sync");
   }
 }
 
 /* static */ bool ChromeUtils::IsPopupTokenUnused(GlobalObject& aGlobal) {
   return PopupBlocker::IsPopupOpeningTokenUnused();
 }
 
+/* static */ double ChromeUtils::LastExternalProtocolIframeAllowed(
+    GlobalObject& aGlobal) {
+  TimeStamp when = PopupBlocker::WhenLastExternalProtocolIframeAllowed();
+  if (when.IsNull()) {
+    return 0;
+  }
+
+  TimeDuration duration = TimeStamp::Now() - when;
+  return duration.ToMilliseconds();
+}
+
 /* static */ void ChromeUtils::RegisterWindowActor(
     const GlobalObject& aGlobal, const nsAString& aName,
     const WindowActorOptions& aOptions, ErrorResult& aRv) {
   MOZ_ASSERT(XRE_IsParentProcess());
 
   RefPtr<JSWindowActorService> service = JSWindowActorService::GetSingleton();
   service->RegisterWindowActor(aName, aOptions, aRv);
 }
--- a/dom/base/ChromeUtils.h
+++ b/dom/base/ChromeUtils.h
@@ -173,16 +173,18 @@ class ChromeUtils {
   static bool HasReportingHeaderForOrigin(GlobalObject& global,
                                           const nsAString& aOrigin,
                                           ErrorResult& aRv);
 
   static PopupBlockerState GetPopupControlState(GlobalObject& aGlobal);
 
   static bool IsPopupTokenUnused(GlobalObject& aGlobal);
 
+  static double LastExternalProtocolIframeAllowed(GlobalObject& aGlobal);
+
   static void RegisterWindowActor(const GlobalObject& aGlobal,
                                   const nsAString& aName,
                                   const WindowActorOptions& aOptions,
                                   ErrorResult& aRv);
 };
 
 }  // namespace dom
 }  // namespace mozilla
--- a/dom/base/PopupBlocker.cpp
+++ b/dom/base/PopupBlocker.cpp
@@ -2,31 +2,35 @@
 /* vim: set ts=8 sts=2 et sw=2 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/dom/PopupBlocker.h"
 #include "mozilla/EventStateManager.h"
 #include "mozilla/Preferences.h"
+#include "mozilla/StaticPrefs.h"
 #include "mozilla/TextEvents.h"
+#include "mozilla/TimeStamp.h"
 #include "nsXULPopupManager.h"
 #include "nsIPermissionManager.h"
 
 namespace mozilla {
 namespace dom {
 
 namespace {
 
 static char* sPopupAllowedEvents;
 
 static PopupBlocker::PopupControlState sPopupControlState =
     PopupBlocker::openAbused;
 static uint32_t sPopupStatePusherCount = 0;
 
+static TimeStamp sLastAllowedExternalProtocolIFrameTimeStamp;
+
 // This token is by default set to false. When a popup/filePicker is shown, it
 // is set to true.
 static bool sUnusedPopupToken = false;
 
 void PopupAllowedEventsChanged() {
   if (sPopupAllowedEvents) {
     free(sPopupAllowedEvents);
   }
@@ -374,16 +378,37 @@ PopupBlocker::PopupControlState PopupBlo
 /* static */ void PopupBlocker::Shutdown() {
   if (sPopupAllowedEvents) {
     free(sPopupAllowedEvents);
   }
 
   Preferences::UnregisterCallback(OnPrefChange, "dom.popup_allowed_events");
 }
 
+/* static */ bool PopupBlocker::ConsumeTimerTokenForExternalProtocolIframe() {
+  TimeStamp now = TimeStamp::Now();
+
+  if (sLastAllowedExternalProtocolIFrameTimeStamp.IsNull()) {
+    sLastAllowedExternalProtocolIFrameTimeStamp = now;
+    return true;
+  }
+
+  if ((now - sLastAllowedExternalProtocolIFrameTimeStamp).ToSeconds() <
+      (StaticPrefs::dom_delay_block_external_protocol_in_iframes())) {
+    return false;
+  }
+
+  sLastAllowedExternalProtocolIFrameTimeStamp = now;
+  return true;
+}
+
+/* static */ TimeStamp PopupBlocker::WhenLastExternalProtocolIframeAllowed() {
+  return sLastAllowedExternalProtocolIFrameTimeStamp;
+}
+
 }  // namespace dom
 }  // namespace mozilla
 
 nsAutoPopupStatePusherInternal::nsAutoPopupStatePusherInternal(
     mozilla::dom::PopupBlocker::PopupControlState aState, bool aForce)
     : mOldState(
           mozilla::dom::PopupBlocker::PushPopupControlState(aState, aForce)) {
   mozilla::dom::PopupBlocker::PopupStatePusherCreated();
--- a/dom/base/PopupBlocker.h
+++ b/dom/base/PopupBlocker.h
@@ -48,16 +48,22 @@ class PopupBlocker final {
   // allowed per event.
   static bool TryUsePopupOpeningToken();
 
   static bool IsPopupOpeningTokenUnused();
 
   static PopupBlocker::PopupControlState GetEventPopupControlState(
       WidgetEvent* aEvent, Event* aDOMEvent = nullptr);
 
+  // Returns if a external protocol iframe is allowed.
+  static bool ConsumeTimerTokenForExternalProtocolIframe();
+
+  // Returns when the last external protocol iframe has been allowed.
+  static TimeStamp WhenLastExternalProtocolIframeAllowed();
+
   static void Initialize();
   static void Shutdown();
 };
 
 }  // namespace dom
 }  // namespace mozilla
 
 #ifdef MOZILLA_INTERNAL_API
--- a/dom/chrome-webidl/ChromeUtils.webidl
+++ b/dom/chrome-webidl/ChromeUtils.webidl
@@ -381,16 +381,22 @@ partial namespace ChromeUtils {
   boolean hasReportingHeaderForOrigin(DOMString aOrigin);
 
   [ChromeOnly]
   PopupBlockerState getPopupControlState();
 
   [ChromeOnly]
   boolean isPopupTokenUnused();
 
+  /**
+   * Milliseconds from the last iframe loading an external protocol.
+   */
+  [ChromeOnly]
+  double lastExternalProtocolIframeAllowed();
+
   [ChromeOnly, Throws]
   void registerWindowActor(DOMString aName, WindowActorOptions aOptions);
 };
 
 /**
  * Dictionaries duplicating IPDL types in dom/ipc/DOMTypes.ipdlh
  * Used by requestPerformanceMetrics
  */
--- a/dom/html/test/test_external_protocol_iframe.html
+++ b/dom/html/test/test_external_protocol_iframe.html
@@ -5,40 +5,73 @@
   <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
   <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
   <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
 </head>
 <body>
   <div id='foo'><a href='#'>Click here to test this issue</a></div>
   <script>
 
-function next() {
+function test_noUserInteraction() {
+  is(ChromeUtils.getPopupControlState(), "openAbused", "No user-interaction means: abuse state");
+  ok(!ChromeUtils.isPopupTokenUnused(), "Popup token has not been used yet");
+  is(ChromeUtils.lastExternalProtocolIframeAllowed(), 0, "No iframe loaded before this test!");
+
+  for (let i = 0; i < 10; ++i) {
+    let ifr = document.createElement('iframe');
+    ifr.src = "foo+bar:all_good";
+    document.body.appendChild(ifr);
+
+    is(ChromeUtils.getPopupControlState(), "openAbused", "No user-interaction means: abuse state");
+    ok(!ChromeUtils.isPopupTokenUnused(), "Popup token has been used!");
+    ok(ChromeUtils.lastExternalProtocolIframeAllowed() != 0, "We have 1 iframe loaded");
+  }
+
+  next();
+}
+
+function test_userInteraction() {
   let foo = document.getElementById('foo');
   foo.addEventListener('click', _ => {
     is(ChromeUtils.getPopupControlState(), "openAllowed", "Click events allow popups");
     ok(!ChromeUtils.isPopupTokenUnused(), "Popup token has not been used yet");
 
     for (let i = 0; i < 10; ++i) {
       let ifr = document.createElement('iframe');
       ifr.src = "foo+bar:all_good";
       document.body.appendChild(ifr);
 
       is(ChromeUtils.getPopupControlState(), "openAllowed", "Click events allow popups");
       ok(ChromeUtils.isPopupTokenUnused(), "Popup token has been used!");
     }
 
-    SimpleTest.finish();
+    next();
 
   }, {once: true});
 
   setTimeout(_ => {
     sendMouseEvent({type:'click'}, 'foo');
   }, 0);
 }
 
+let tests = [
+  test_noUserInteraction,
+  test_userInteraction,
+];
+
+function next() {
+  if (tests.length == 0) {
+    SimpleTest.finish();
+    return;
+  }
+
+  let test = tests.shift();
+  SimpleTest.executeSoon(test);
+}
+
 SpecialPowers.pushPrefEnv({'set': [
   ['dom.block_external_protocol_in_iframes', true],
 ]}, next);
 
 SimpleTest.waitForExplicitFinish();
   </script>
 </body>
 </html>
--- a/modules/libpref/init/StaticPrefList.h
+++ b/modules/libpref/init/StaticPrefList.h
@@ -464,16 +464,24 @@ VARCACHE_PREF(
 #endif
 VARCACHE_PREF(
   "dom.block_external_protocol_in_iframes",
    dom_block_external_protocol_in_iframes,
   bool, PREF_VALUE
 )
 #undef PREF_VALUE
 
+// Any how many seconds we allow external protocol URLs in iframe when not in
+// single events
+VARCACHE_PREF(
+  "dom.delay.block_external_protocol_in_iframes",
+   dom_delay_block_external_protocol_in_iframes,
+  uint32_t, 10 // in seconds
+)
+
 // Block multiple window.open() per single event.
 VARCACHE_PREF(
   "dom.block_multiple_popups",
    dom_block_multiple_popups,
   bool, true
 )
 
 // For area and anchor elements with target=_blank and no rel set to