Bug 1312243 - Block authentication dialog pop-ups by cancling 3 of them in a row, r=Dolske+mcmanus a=jcristau
authorHonza Bambas <honzab.moz@firemni.cz>
Tue, 20 Dec 2016 08:27:00 +0100
changeset 353168 fec44289a5e263cdd6bb7670160b32d6aa8f3486
parent 353167 a368c15c83ace70e2e9a95a308a876e7689b4a8c
child 353169 ea647f733669fd445b683ef626cf0904499d5d7d
push id6795
push userjlund@mozilla.com
push dateMon, 23 Jan 2017 14:19:46 +0000
treeherdermozilla-esr52@76101b503191 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersDolske, jcristau
bugs1312243
milestone52.0a2
Bug 1312243 - Block authentication dialog pop-ups by cancling 3 of them in a row, r=Dolske+mcmanus a=jcristau
modules/libpref/init/all.js
netwerk/ipc/NeckoChannelParams.ipdlh
netwerk/protocol/http/HttpBaseChannel.cpp
netwerk/protocol/http/HttpBaseChannel.h
netwerk/protocol/http/HttpChannelChild.cpp
netwerk/protocol/http/HttpChannelParent.cpp
netwerk/protocol/http/HttpChannelParent.h
netwerk/protocol/http/NullHttpChannel.cpp
netwerk/protocol/http/nsIHttpChannel.idl
netwerk/protocol/viewsource/nsViewSourceChannel.cpp
toolkit/components/passwordmgr/nsLoginManagerPrompter.js
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -5539,8 +5539,14 @@ pref("security.mixed_content.use_hsts", 
 pref ("security.mixed_content.hsts_priming_cache_timeout", 10080);
 
 // Disable Storage api in release builds.
 #ifdef NIGHTLY_BUILD
 pref("dom.storageManager.enabled", true);
 #else
 pref("dom.storageManager.enabled", false);
 #endif
+
+// When a user cancels this number of authentication dialogs coming from
+// a single web page in a row, all following authentication dialogs will
+// be blocked (automatically canceled) for that page. The counter resets
+// when the page is reloaded. To turn this feature off, just set the limit to 0.
+pref("prompts.authentication_dialog_abuse_limit", 3);
--- a/netwerk/ipc/NeckoChannelParams.ipdlh
+++ b/netwerk/ipc/NeckoChannelParams.ipdlh
@@ -125,16 +125,17 @@ struct HttpChannelOpenArgs
   nsCString                   requestContextID;
   OptionalCorsPreflightArgs   preflightArgs;
   uint32_t                    initialRwin;
   bool                        blockAuthPrompt;
   bool                        suspendAfterSynthesizeResponse;
   bool                        allowStaleCacheContent;
   nsCString                   contentTypeHint;
   nsCString                   channelId;
+  uint64_t                    contentWindowId;
   nsCString                   preferredAlternativeType;
 };
 
 struct HttpChannelConnectArgs
 {
   uint32_t registrarId;
   bool shouldIntercept;
 };
--- a/netwerk/protocol/http/HttpBaseChannel.cpp
+++ b/netwerk/protocol/http/HttpBaseChannel.cpp
@@ -50,18 +50,20 @@
 #include "nsISSLSocketControl.h"
 #include "mozilla/Telemetry.h"
 #include "nsIURL.h"
 #include "nsIConsoleService.h"
 #include "mozilla/BinarySearch.h"
 #include "nsIHttpHeaderVisitor.h"
 #include "nsIXULRuntime.h"
 #include "nsICacheInfoChannel.h"
+#include "nsIDOMWindowUtils.h"
 
 #include <algorithm>
+#include "HttpBaseChannel.h"
 
 namespace mozilla {
 namespace net {
 
 HttpBaseChannel::HttpBaseChannel()
   : mStartPos(UINT64_MAX)
   , mStatus(NS_OK)
   , mLoadFlags(LOAD_NORMAL)
@@ -108,16 +110,17 @@ HttpBaseChannel::HttpBaseChannel()
   , mRedirectMode(nsIHttpChannelInternal::REDIRECT_MODE_FOLLOW)
   , mFetchCacheMode(nsIHttpChannelInternal::FETCH_CACHE_MODE_DEFAULT)
   , mOnStartRequestCalled(false)
   , mOnStopRequestCalled(false)
   , mAfterOnStartRequestBegun(false)
   , mTransferSize(0)
   , mDecodedBodySize(0)
   , mEncodedBodySize(0)
+  , mContentWindowId(0)
   , mRequireCORSPreflight(false)
   , mReportCollector(new ConsoleReportCollector())
   , mForceMainDocumentChannel(false)
 {
   LOG(("Creating HttpBaseChannel @%x\n", this));
 
   // Subfields of unions cannot be targeted in an initializer list.
 #ifdef MOZ_VALGRIND
@@ -1191,16 +1194,40 @@ HttpBaseChannel::SetChannelId(const nsAC
   if (newId.Parse(idStr.get())) {
     mChannelId = newId;
     return NS_OK;
   }
 
   return NS_ERROR_FAILURE;
 }
 
+NS_IMETHODIMP HttpBaseChannel::GetTopLevelContentWindowId(uint64_t *aWindowId)
+{
+  if (!mContentWindowId) {
+    nsCOMPtr<nsILoadContext> loadContext;
+    GetCallback(loadContext);
+    if (loadContext) {
+      nsCOMPtr<mozIDOMWindowProxy> topWindow;
+      loadContext->GetTopWindow(getter_AddRefs(topWindow));
+      nsCOMPtr<nsIDOMWindowUtils> windowUtils = do_GetInterface(topWindow);
+      if (windowUtils) {
+        windowUtils->GetCurrentInnerWindowID(&mContentWindowId);
+      }
+    }
+  }
+  *aWindowId = mContentWindowId;
+  return NS_OK;
+}
+
+NS_IMETHODIMP HttpBaseChannel::SetTopLevelContentWindowId(uint64_t aWindowId)
+{
+  mContentWindowId = aWindowId;
+  return NS_OK;
+}
+
 NS_IMETHODIMP
 HttpBaseChannel::GetTransferSize(uint64_t *aTransferSize)
 {
   *aTransferSize = mTransferSize;
   return NS_OK;
 }
 
 NS_IMETHODIMP
--- a/netwerk/protocol/http/HttpBaseChannel.h
+++ b/netwerk/protocol/http/HttpBaseChannel.h
@@ -183,16 +183,18 @@ public:
   NS_IMETHOD GetDecodedBodySize(uint64_t *aDecodedBodySize) override;
   NS_IMETHOD GetEncodedBodySize(uint64_t *aEncodedBodySize) override;
   NS_IMETHOD SetRequestContextID(const nsID aRCID) override;
   NS_IMETHOD GetIsMainDocumentChannel(bool* aValue) override;
   NS_IMETHOD SetIsMainDocumentChannel(bool aValue) override;
   NS_IMETHOD GetProtocolVersion(nsACString & aProtocolVersion) override;
   NS_IMETHOD GetChannelId(nsACString& aChannelId) override;
   NS_IMETHOD SetChannelId(const nsACString& aChannelId) override;
+  NS_IMETHOD GetTopLevelContentWindowId(uint64_t *aContentWindowId) override;
+  NS_IMETHOD SetTopLevelContentWindowId(uint64_t aContentWindowId) override;
 
   // nsIHttpChannelInternal
   NS_IMETHOD GetDocumentURI(nsIURI **aDocumentURI) override;
   NS_IMETHOD SetDocumentURI(nsIURI *aDocumentURI) override;
   NS_IMETHOD GetRequestVersion(uint32_t *major, uint32_t *minor) override;
   NS_IMETHOD GetResponseVersion(uint32_t *major, uint32_t *minor) override;
   NS_IMETHOD SetCookie(const char *aCookieHeader) override;
   NS_IMETHOD GetThirdPartyFlags(uint32_t *aForce) override;
@@ -545,16 +547,20 @@ protected:
   uint64_t mEncodedBodySize;
 
   // The network interface id that's associated with this channel.
   nsCString mNetworkInterfaceId;
 
   nsID mRequestContextID;
   bool EnsureRequestContextID();
 
+  // ID of the top-level document's inner window this channel is being
+  // originated from.
+  uint64_t mContentWindowId;
+
   bool                              mRequireCORSPreflight;
   nsTArray<nsCString>               mUnsafeHeaders;
 
   nsCOMPtr<nsIConsoleReportCollector> mReportCollector;
 
   // Holds the name of the preferred alt-data type.
   nsCString mPreferredCachedAltDataType;
   // Holds the name of the alternative data type the channel returned.
--- a/netwerk/protocol/http/HttpChannelChild.cpp
+++ b/netwerk/protocol/http/HttpChannelChild.cpp
@@ -33,16 +33,18 @@
 #include "mozilla/net/DNS.h"
 #include "SerializedLoadContext.h"
 #include "nsInputStreamPump.h"
 #include "InterceptedChannel.h"
 #include "mozIThirdPartyUtil.h"
 #include "nsContentSecurityManager.h"
 #include "nsIDeprecationWarner.h"
 #include "nsICompressConvStats.h"
+#include "nsIDocument.h"
+#include "nsIDOMWindowUtils.h"
 #include "nsStreamUtils.h"
 
 #ifdef OS_POSIX
 #include "chrome/common/file_descriptor_set_posix.h"
 #endif
 
 using namespace mozilla::dom;
 using namespace mozilla::ipc;
@@ -2009,16 +2011,28 @@ HttpChannelChild::ContinueAsyncOpen()
   GetCallback(iTabChild);
   if (iTabChild) {
     tabChild = static_cast<mozilla::dom::TabChild*>(iTabChild.get());
   }
   if (MissingRequiredTabChild(tabChild, "http")) {
     return NS_ERROR_ILLEGAL_VALUE;
   }
 
+  // This id identifies the inner window's top-level document,
+  // which changes on every new load or navigation.
+  uint64_t contentWindowId = 0;
+  if (tabChild) {
+    MOZ_ASSERT(tabChild->WebNavigation());
+    nsCOMPtr<nsIDocument> document = tabChild->GetDocument();
+    if (document) {
+      contentWindowId = document->InnerWindowID();
+    }
+  }
+  SetTopLevelContentWindowId(contentWindowId);
+
   HttpChannelOpenArgs openArgs;
   // No access to HttpChannelOpenArgs members, but they each have a
   // function with the struct name that returns a ref.
   SerializeURI(mURI, openArgs.uri());
   SerializeURI(mOriginalURI, openArgs.original());
   SerializeURI(mDocumentURI, openArgs.doc());
   SerializeURI(mReferrer, openArgs.referrer());
   openArgs.referrerPolicy() = mReferrerPolicy;
@@ -2103,16 +2117,18 @@ HttpChannelChild::ContinueAsyncOpen()
   char rcid[NSID_LENGTH];
   mRequestContextID.ToProvidedString(rcid);
   openArgs.requestContextID().AssignASCII(rcid);
 
   char chid[NSID_LENGTH];
   mChannelId.ToProvidedString(chid);
   openArgs.channelId().AssignASCII(chid);
 
+  openArgs.contentWindowId() = contentWindowId;
+
   if (tabChild && !tabChild->IPCOpen()) {
     return NS_ERROR_FAILURE;
   }
 
   ContentChild* cc = static_cast<ContentChild*>(gNeckoChild->Manager());
   if (cc->IsShuttingDown()) {
     return NS_ERROR_FAILURE;
   }
--- a/netwerk/protocol/http/HttpChannelParent.cpp
+++ b/netwerk/protocol/http/HttpChannelParent.cpp
@@ -125,17 +125,17 @@ HttpChannelParent::Init(const HttpChanne
                        a.entityID(), a.chooseApplicationCache(),
                        a.appCacheClientID(), a.allowSpdy(), a.allowAltSvc(), a.beConservative(),
                        a.loadInfo(), a.synthesizedResponseHead(),
                        a.synthesizedSecurityInfoSerialization(),
                        a.cacheKey(), a.requestContextID(), a.preflightArgs(),
                        a.initialRwin(), a.blockAuthPrompt(),
                        a.suspendAfterSynthesizeResponse(),
                        a.allowStaleCacheContent(), a.contentTypeHint(),
-                       a.channelId(), a.preferredAlternativeType());
+                       a.channelId(), a.contentWindowId(), a.preferredAlternativeType());
   }
   case HttpChannelCreationArgs::THttpChannelConnectArgs:
   {
     const HttpChannelConnectArgs& cArgs = aArgs.get_HttpChannelConnectArgs();
     return ConnectChannel(cArgs.registrarId(), cArgs.shouldIntercept());
   }
   default:
     NS_NOTREACHED("unknown open type");
@@ -323,16 +323,17 @@ HttpChannelParent::DoAsyncOpen(  const U
                                  const nsCString&           aRequestContextID,
                                  const OptionalCorsPreflightArgs& aCorsPreflightArgs,
                                  const uint32_t&            aInitialRwin,
                                  const bool&                aBlockAuthPrompt,
                                  const bool&                aSuspendAfterSynthesizeResponse,
                                  const bool&                aAllowStaleCacheContent,
                                  const nsCString&           aContentTypeHint,
                                  const nsCString&           aChannelId,
+                                 const uint64_t&            aContentWindowId,
                                  const nsCString&           aPreferredAlternativeType)
 {
   nsCOMPtr<nsIURI> uri = DeserializeURI(aURI);
   if (!uri) {
     // URIParams does MOZ_ASSERT if null, but we need to protect opt builds from
     // null deref here.
     return false;
   }
@@ -372,16 +373,17 @@ HttpChannelParent::DoAsyncOpen(  const U
     return SendFailedAsyncOpen(rv);
 
   // This cast is safe since this is AsyncOpen specific to http.  channel
   // is ensured to be nsHttpChannel.
   mChannel = static_cast<nsHttpChannel *>(channel.get());
 
   // Set the channelId allocated in child to the parent instance
   mChannel->SetChannelId(aChannelId);
+  mChannel->SetTopLevelContentWindowId(aContentWindowId);
 
   mChannel->SetWarningReporter(this);
   mChannel->SetTimingEnabled(true);
   if (mPBOverride != kPBOverride_Unset) {
     mChannel->SetPrivate(mPBOverride == kPBOverride_Private ? true : false);
   }
 
   if (doResumeAt)
--- a/netwerk/protocol/http/HttpChannelParent.h
+++ b/netwerk/protocol/http/HttpChannelParent.h
@@ -137,16 +137,17 @@ protected:
                    const nsCString&           aRequestContextID,
                    const OptionalCorsPreflightArgs& aCorsPreflightArgs,
                    const uint32_t&            aInitialRwin,
                    const bool&                aBlockAuthPrompt,
                    const bool&                aSuspendAfterSynthesizeResponse,
                    const bool&                aAllowStaleCacheContent,
                    const nsCString&           aContentTypeHint,
                    const nsCString&           aChannelId,
+                   const uint64_t&            aContentWindowId,
                    const nsCString&           aPreferredAlternativeType);
 
   virtual bool RecvSetPriority(const uint16_t& priority) override;
   virtual bool RecvSetClassOfService(const uint32_t& cos) override;
   virtual bool RecvSetCacheTokenCachedCharset(const nsCString& charset) override;
   virtual bool RecvSuspend() override;
   virtual bool RecvResume() override;
   virtual bool RecvCancel(const nsresult& status) override;
--- a/netwerk/protocol/http/NullHttpChannel.cpp
+++ b/netwerk/protocol/http/NullHttpChannel.cpp
@@ -65,16 +65,28 @@ NullHttpChannel::GetChannelId(nsACString
 
 NS_IMETHODIMP
 NullHttpChannel::SetChannelId(const nsACString& aChannelId)
 {
     return NS_ERROR_NOT_IMPLEMENTED;
 }
 
 NS_IMETHODIMP
+NullHttpChannel::GetTopLevelContentWindowId(uint64_t *aWindowId)
+{
+    return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+NullHttpChannel::SetTopLevelContentWindowId(uint64_t aWindowId)
+{
+    return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
 NullHttpChannel::GetTransferSize(uint64_t *aTransferSize)
 {
     return NS_ERROR_NOT_IMPLEMENTED;
 }
 
 NS_IMETHODIMP
 NullHttpChannel::GetDecodedBodySize(uint64_t *aDecodedBodySize)
 {
--- a/netwerk/protocol/http/nsIHttpChannel.idl
+++ b/netwerk/protocol/http/nsIHttpChannel.idl
@@ -458,9 +458,15 @@ interface nsIHttpChannel : nsIChannel
     [noscript] attribute nsID requestContextID;
 
     /**
      * Unique ID of the channel, shared between parent and child. Needed if
      * the channel activity needs to be monitored across process boundaries,
      * like in devtools net monitor. See bug 1274556.
      */
     attribute ACString channelId;
+
+    /**
+     * ID of the top-level document's inner window.  Identifies the content
+     * this channels is being load in.
+     */
+    attribute uint64_t topLevelContentWindowId;
 };
--- a/netwerk/protocol/viewsource/nsViewSourceChannel.cpp
+++ b/netwerk/protocol/viewsource/nsViewSourceChannel.cpp
@@ -721,16 +721,30 @@ nsViewSourceChannel::GetChannelId(nsACSt
 NS_IMETHODIMP
 nsViewSourceChannel::SetChannelId(const nsACString& aChannelId)
 {
   return !mHttpChannel ? NS_ERROR_NULL_POINTER :
       mHttpChannel->SetChannelId(aChannelId);
 }
 
 NS_IMETHODIMP
+nsViewSourceChannel::GetTopLevelContentWindowId(uint64_t *aWindowId)
+{
+  return !mHttpChannel ? NS_ERROR_NULL_POINTER :
+      mHttpChannel->GetTopLevelContentWindowId(aWindowId);
+}
+
+NS_IMETHODIMP
+nsViewSourceChannel::SetTopLevelContentWindowId(uint64_t aWindowId)
+{
+  return !mHttpChannel ? NS_ERROR_NULL_POINTER :
+      mHttpChannel->SetTopLevelContentWindowId(aWindowId);
+}
+
+NS_IMETHODIMP
 nsViewSourceChannel::GetRequestMethod(nsACString & aRequestMethod)
 {
     return !mHttpChannel ? NS_ERROR_NULL_POINTER :
         mHttpChannel->GetRequestMethod(aRequestMethod);
 }
 
 NS_IMETHODIMP
 nsViewSourceChannel::SetRequestMethod(const nsACString & aRequestMethod)
--- a/toolkit/components/passwordmgr/nsLoginManagerPrompter.js
+++ b/toolkit/components/passwordmgr/nsLoginManagerPrompter.js
@@ -92,61 +92,99 @@ LoginManagerPromptFactory.prototype = {
       let httpHostname = hostname.replace(/^https:\/\//, "http://");
       hasLogins = (prompter._pwmgr.countLogins(httpHostname, null, httpRealm) > 0);
     }
     if (hasLogins && prompter._pwmgr.uiBusy) {
       this.log("_doAsyncPrompt:run bypassed, master password UI busy");
       return;
     }
 
-    this._asyncPromptInProgress = true;
-    prompt.inProgress = true;
+    // Allow only a limited number of authentication dialogs when they are all
+    // canceled by the user.
+    var cancelationCounter = (prompter._browser && prompter._browser.canceledAuthenticationPromptCounter) || { count: 0, id: 0 };
+    if (prompt.channel) {
+      var httpChannel = prompt.channel.QueryInterface(Ci.nsIHttpChannel);
+      if (httpChannel) {
+        var windowId = httpChannel.topLevelContentWindowId;
+        if (windowId != cancelationCounter.id) {
+          // window has been reloaded or navigated, reset the counter
+          cancelationCounter = { count: 0, id: windowId };
+        }
+      }
+    }
 
     var self = this;
 
     var runnable = {
+      cancel: false,
       run : function() {
         var ok = false;
-        try {
-          self.log("_doAsyncPrompt:run - performing the prompt for '" + hashKey + "'");
-          ok = prompter.promptAuth(prompt.channel,
-                                   prompt.level,
-                                   prompt.authInfo);
-        } catch (e) {
-          if (e instanceof Components.Exception &&
-              e.result == Cr.NS_ERROR_NOT_AVAILABLE) {
-            self.log("_doAsyncPrompt:run bypassed, UI is not available in this context");
+        if (!this.cancel) {
+          try {
+            self.log("_doAsyncPrompt:run - performing the prompt for '" + hashKey + "'");
+            ok = prompter.promptAuth(prompt.channel,
+                                     prompt.level,
+                                     prompt.authInfo);
+          } catch (e) {
+            if (e instanceof Components.Exception &&
+                e.result == Cr.NS_ERROR_NOT_AVAILABLE) {
+              self.log("_doAsyncPrompt:run bypassed, UI is not available in this context");
+            } else {
+              Components.utils.reportError("LoginManagerPrompter: " +
+                                           "_doAsyncPrompt:run: " + e + "\n");
+            }
+          }
+
+          delete self._asyncPrompts[hashKey];
+          prompt.inProgress = false;
+          self._asyncPromptInProgress = false;
+
+          if (ok) {
+            cancelationCounter.count = 0;
           } else {
-            Components.utils.reportError("LoginManagerPrompter: " +
-                                         "_doAsyncPrompt:run: " + e + "\n");
+            cancelationCounter.count++;
+          }
+          if (prompter._browser) {
+            prompter._browser.canceledAuthenticationPromptCounter = cancelationCounter;
           }
         }
 
-        delete self._asyncPrompts[hashKey];
-        prompt.inProgress = false;
-        self._asyncPromptInProgress = false;
-
         for (var consumer of prompt.consumers) {
           if (!consumer.callback)
             // Not having a callback means that consumer didn't provide it
             // or canceled the notification
             continue;
 
           self.log("Calling back to " + consumer.callback + " ok=" + ok);
           try {
-            if (ok)
+            if (ok) {
               consumer.callback.onAuthAvailable(consumer.context, prompt.authInfo);
-            else
-              consumer.callback.onAuthCancelled(consumer.context, true);
+            } else {
+              consumer.callback.onAuthCancelled(consumer.context, !this.cancel);
+            }
           } catch (e) { /* Throw away exceptions caused by callback */ }
         }
         self._doAsyncPrompt();
       }
     };
 
+    var cancelDialogLimit = Services.prefs.getIntPref("prompts.authentication_dialog_abuse_limit");
+
+    this.log("cancelationCounter =", cancelationCounter);
+    if (cancelDialogLimit && cancelationCounter.count >= cancelDialogLimit) {
+      this.log("Blocking auth dialog, due to exceeding dialog bloat limit");
+      delete this._asyncPrompts[hashKey];
+
+      // just make the runnable cancel all consumers
+      runnable.cancel = true;
+    } else {
+      this._asyncPromptInProgress = true;
+      prompt.inProgress = true;
+    }
+
     Services.tm.mainThread.dispatch(runnable, Ci.nsIThread.DISPATCH_NORMAL);
     this.log("_doAsyncPrompt:run dispatched");
   },
 
 
   _cancelPendingPrompts : function() {
     this.log("Canceling all pending prompts...");
     var asyncPrompts = this._asyncPrompts;