From d60d9721fd37b6f7af0877d82f14c091c153a0c0 Mon Sep 17 00:00:00 2001 From: Cameron Kaiser Date: Tue, 26 Mar 2019 22:23:40 -0700 Subject: [PATCH] #547: block modal HTTP auth DOS M1312243 M377496 + glue code --- browser/base/content/browser.js | 4 + browser/base/content/tabbrowser.xml | 1 + browser/base/content/urlbarBindings.xml | 1 + docshell/base/nsDocShell.cpp | 30 ++++++ modules/libpref/init/all.js | 6 ++ netwerk/ipc/NeckoChannelParams.ipdlh | 1 + netwerk/protocol/http/HttpBaseChannel.cpp | 27 ++++++ netwerk/protocol/http/HttpBaseChannel.h | 6 ++ netwerk/protocol/http/HttpChannelChild.cpp | 16 ++++ netwerk/protocol/http/HttpChannelParent.cpp | 7 +- netwerk/protocol/http/HttpChannelParent.h | 3 +- netwerk/protocol/http/NullHttpChannel.cpp | 12 +++ netwerk/protocol/http/nsIHttpChannel.idl | 6 ++ .../viewsource/nsViewSourceChannel.cpp | 15 +++ .../passwordmgr/nsLoginManagerPrompter.js | 95 +++++++++++++++---- 15 files changed, 207 insertions(+), 23 deletions(-) diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js index 93966140c..10d431181 100644 --- a/browser/base/content/browser.js +++ b/browser/base/content/browser.js @@ -3259,6 +3259,10 @@ function BrowserReloadWithFlags(reloadFlags) { return; } + // Reset temporary permissions on the current tab. This is done here + // because we only want to reset permissions on user reload. + delete gBrowser.selectedBrowser.canceledAuthenticationPromptCounter; + let windowUtils = window.QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIDOMWindowUtils); diff --git a/browser/base/content/tabbrowser.xml b/browser/base/content/tabbrowser.xml index 25e54d70b..7aba77445 100644 --- a/browser/base/content/tabbrowser.xml +++ b/browser/base/content/tabbrowser.xml @@ -2679,6 +2679,7 @@ let remote = false; diff --git a/browser/base/content/urlbarBindings.xml b/browser/base/content/urlbarBindings.xml index e684b0b00..4f42e93fc 100644 --- a/browser/base/content/urlbarBindings.xml +++ b/browser/base/content/urlbarBindings.xml @@ -417,6 +417,7 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/. // occurs in a new tab, we want focus to be restored to the content // area when the current tab is re-selected. gBrowser.selectedBrowser.focus(); + delete gBrowser.selectedBrowser.canceledAuthenticationPromptCounter; let isMouseEvent = aTriggeringEvent instanceof MouseEvent; diff --git a/docshell/base/nsDocShell.cpp b/docshell/base/nsDocShell.cpp index 58fdff12d..b18e1d4b2 100644 --- a/docshell/base/nsDocShell.cpp +++ b/docshell/base/nsDocShell.cpp @@ -84,6 +84,7 @@ #include "nsWhitespaceTokenizer.h" #include "nsICookieService.h" #include "nsIConsoleReportCollector.h" +#include "nsILoginManagerPrompter.h" // we want to explore making the document own the load group // so we can associate the document URI with the load group. @@ -13169,6 +13170,35 @@ nsDocShell::GetAuthPrompt(uint32_t aPromptReason, const nsIID& aIID, // Get the an auth prompter for our window so that the parenting // of the dialogs works as it should when using tabs. + // Since we don't go through E10S, we need to set the browser element + // manually (a la dom/ipc/TabParent) so that we can check elements + // that may be set on it, such as HTTP Auth DOS (TenFourFox issue 547). + + NS_ASSERTION(mScriptGlobal, "We don't have a script global"); + if (MOZ_LIKELY(mScriptGlobal)) { + nsCOMPtr frameElement = mScriptGlobal->GetFrameElementInternal(); + if (MOZ_LIKELY(frameElement)) { + nsCOMPtr frame = do_QueryInterface(frameElement); + if (MOZ_LIKELY(frame)) { + nsCOMPtr prompt; + nsCOMPtr window = do_QueryInterface(frame->OwnerDoc()->GetWindow()); + if (MOZ_LIKELY(window) && + NS_SUCCEEDED(wwatch->GetPrompt(window, aIID, + getter_AddRefs(prompt))) && + MOZ_LIKELY(prompt)) { + nsCOMPtr browser = do_QueryInterface(frameElement); + if (MOZ_LIKELY(browser)) { + nsCOMPtr prompter = do_QueryInterface(prompt); + if (MOZ_LIKELY(prompter)) + prompter->SetE10sData(browser, nullptr); + } + *aResult = prompt.forget().take(); + return NS_OK; + } + } + } + NS_WARNING("Unable to connect browser to auth prompt, falling back"); + } return wwatch->GetPrompt(mScriptGlobal, aIID, reinterpret_cast(aResult)); diff --git a/modules/libpref/init/all.js b/modules/libpref/init/all.js index 85975fdc2..88839b013 100644 --- a/modules/libpref/init/all.js +++ b/modules/libpref/init/all.js @@ -5151,6 +5151,12 @@ pref("toolkit.pageThumbs.screenSizeDivisor", 7); pref("toolkit.pageThumbs.minWidth", 0); pref("toolkit.pageThumbs.minHeight", 0); +// 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); + pref("tenfourfox.adblock.enabled", false); pref("tenfourfox.adblock.logging.enabled", false); pref("tenfourfox.dom.forms.date", true); diff --git a/netwerk/ipc/NeckoChannelParams.ipdlh b/netwerk/ipc/NeckoChannelParams.ipdlh index abad41380..250e3a581 100644 --- a/netwerk/ipc/NeckoChannelParams.ipdlh +++ b/netwerk/ipc/NeckoChannelParams.ipdlh @@ -117,6 +117,7 @@ struct HttpChannelOpenArgs OptionalCorsPreflightArgs preflightArgs; uint32_t initialRwin; bool suspendAfterSynthesizeResponse; + uint64_t contentWindowId; }; struct HttpChannelConnectArgs diff --git a/netwerk/protocol/http/HttpBaseChannel.cpp b/netwerk/protocol/http/HttpBaseChannel.cpp index dd4dc6e29..89784819e 100644 --- a/netwerk/protocol/http/HttpBaseChannel.cpp +++ b/netwerk/protocol/http/HttpBaseChannel.cpp @@ -48,8 +48,10 @@ #include "LoadInfo.h" #include "nsIXULRuntime.h" #include "nsPIDOMWindow.h" +#include "nsIDOMWindowUtils.h" #include +#include "HttpBaseChannel.h" namespace mozilla { namespace net { @@ -100,6 +102,7 @@ HttpBaseChannel::HttpBaseChannel() , mTransferSize(0) , mDecodedBodySize(0) , mEncodedBodySize(0) + , mContentWindowId(0) , mRequireCORSPreflight(false) , mReportCollector(new ConsoleReportCollector()) , mForceMainDocumentChannel(false) @@ -1100,6 +1103,30 @@ HttpBaseChannel::nsContentEncodings::PrepareForNext(void) // HttpBaseChannel::nsIHttpChannel //----------------------------------------------------------------------------- +NS_IMETHODIMP HttpBaseChannel::GetTopLevelContentWindowId(uint64_t *aWindowId) +{ + if (!mContentWindowId) { + nsCOMPtr loadContext; + GetCallback(loadContext); + if (loadContext) { + nsCOMPtr topWindow; + loadContext->GetTopWindow(getter_AddRefs(topWindow)); + nsCOMPtr 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) { diff --git a/netwerk/protocol/http/HttpBaseChannel.h b/netwerk/protocol/http/HttpBaseChannel.h index 54b7939c9..11fdfb4c6 100644 --- a/netwerk/protocol/http/HttpBaseChannel.h +++ b/netwerk/protocol/http/HttpBaseChannel.h @@ -178,6 +178,8 @@ public: NS_IMETHOD GetIsMainDocumentChannel(bool* aValue) override; NS_IMETHOD SetIsMainDocumentChannel(bool aValue) override; NS_IMETHOD GetProtocolVersion(nsACString & aProtocolVersion) override; + NS_IMETHOD GetTopLevelContentWindowId(uint64_t *aContentWindowId) override; + NS_IMETHOD SetTopLevelContentWindowId(uint64_t aContentWindowId) override; // nsIHttpChannelInternal NS_IMETHOD GetDocumentURI(nsIURI **aDocumentURI) override; @@ -496,6 +498,10 @@ protected: nsID mSchedulingContextID; bool EnsureSchedulingContextID(); + // ID of the top-level document's inner window this channel is being + // originated from. + uint64_t mContentWindowId; + bool mRequireCORSPreflight; nsTArray mUnsafeHeaders; diff --git a/netwerk/protocol/http/HttpChannelChild.cpp b/netwerk/protocol/http/HttpChannelChild.cpp index 429934a49..75e3adbc9 100644 --- a/netwerk/protocol/http/HttpChannelChild.cpp +++ b/netwerk/protocol/http/HttpChannelChild.cpp @@ -36,6 +36,8 @@ #include "nsContentSecurityManager.h" #include "nsIDeprecationWarner.h" #include "nsICompressConvStats.h" +#include "nsIDocument.h" +#include "nsIDOMWindowUtils.h" #ifdef OS_POSIX #include "chrome/common/file_descriptor_set_posix.h" @@ -1870,6 +1872,18 @@ HttpChannelChild::ContinueAsyncOpen() 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 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. @@ -1970,6 +1984,8 @@ HttpChannelChild::ContinueAsyncOpen() mSchedulingContextID.ToProvidedString(scid); openArgs.schedulingContextID().AssignASCII(scid); + openArgs.contentWindowId() = contentWindowId; + // The socket transport in the chrome process now holds a logical ref to us // until OnStopRequest, or we do a redirect, or we hit an IPDL error. AddIPDLReference(); diff --git a/netwerk/protocol/http/HttpChannelParent.cpp b/netwerk/protocol/http/HttpChannelParent.cpp index abb8d0de6..9cd132b55 100644 --- a/netwerk/protocol/http/HttpChannelParent.cpp +++ b/netwerk/protocol/http/HttpChannelParent.cpp @@ -133,7 +133,8 @@ HttpChannelParent::Init(const HttpChannelCreationArgs& aArgs) a.loadInfo(), a.synthesizedResponseHead(), a.synthesizedSecurityInfoSerialization(), a.cacheKey(), a.schedulingContextID(), a.preflightArgs(), - a.initialRwin(), a.suspendAfterSynthesizeResponse()); + a.initialRwin(), a.suspendAfterSynthesizeResponse(), + a.contentWindowId()); } case HttpChannelCreationArgs::THttpChannelConnectArgs: { @@ -386,7 +387,8 @@ HttpChannelParent::DoAsyncOpen( const URIParams& aURI, const nsCString& aSchedulingContextID, const OptionalCorsPreflightArgs& aCorsPreflightArgs, const uint32_t& aInitialRwin, - const bool& aSuspendAfterSynthesizeResponse) + const bool& aSuspendAfterSynthesizeResponse, + const uint64_t& aContentWindowId) { nsCOMPtr uri = DeserializeURI(aURI); if (!uri) { @@ -440,6 +442,7 @@ HttpChannelParent::DoAsyncOpen( const URIParams& aURI, return SendFailedAsyncOpen(rv); mChannel = static_cast(channel.get()); + mChannel->SetTopLevelContentWindowId(aContentWindowId); mChannel->SetWarningReporter(this); mChannel->SetTimingEnabled(true); if (mPBOverride != kPBOverride_Unset) { diff --git a/netwerk/protocol/http/HttpChannelParent.h b/netwerk/protocol/http/HttpChannelParent.h index f75fc1b92..2d3077202 100644 --- a/netwerk/protocol/http/HttpChannelParent.h +++ b/netwerk/protocol/http/HttpChannelParent.h @@ -129,7 +129,8 @@ protected: const nsCString& aSchedulingContextID, const OptionalCorsPreflightArgs& aCorsPreflightArgs, const uint32_t& aInitialRwin, - const bool& aSuspendAfterSynthesizeResponse); + const bool& aSuspendAfterSynthesizeResponse, + const uint64_t& aContentWindowId); virtual bool RecvSetPriority(const uint16_t& priority) override; virtual bool RecvSetClassOfService(const uint32_t& cos) override; diff --git a/netwerk/protocol/http/NullHttpChannel.cpp b/netwerk/protocol/http/NullHttpChannel.cpp index 39a9dd76b..eaa02b0ad 100644 --- a/netwerk/protocol/http/NullHttpChannel.cpp +++ b/netwerk/protocol/http/NullHttpChannel.cpp @@ -56,6 +56,18 @@ NullHttpChannel::Init(nsIURI *aURI, // NullHttpChannel::nsIHttpChannel //----------------------------------------------------------------------------- +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) { diff --git a/netwerk/protocol/http/nsIHttpChannel.idl b/netwerk/protocol/http/nsIHttpChannel.idl index 220ff10ed..7e1158c55 100644 --- a/netwerk/protocol/http/nsIHttpChannel.idl +++ b/netwerk/protocol/http/nsIHttpChannel.idl @@ -384,4 +384,10 @@ interface nsIHttpChannel : nsIChannel * Identifies the scheduling context for this load. */ [noscript] attribute nsID schedulingContextID; + + /** + * ID of the top-level document's inner window. Identifies the content + * this channel is being loaded in. + */ + attribute uint64_t topLevelContentWindowId; }; diff --git a/netwerk/protocol/viewsource/nsViewSourceChannel.cpp b/netwerk/protocol/viewsource/nsViewSourceChannel.cpp index 5ae50719c..987acea6e 100644 --- a/netwerk/protocol/viewsource/nsViewSourceChannel.cpp +++ b/netwerk/protocol/viewsource/nsViewSourceChannel.cpp @@ -721,6 +721,21 @@ nsViewSourceChannel::OnDataAvailable(nsIRequest *aRequest, nsISupports* aContext // We want to forward most of nsIHttpChannel over to mHttpChannel, but we want // to override GetRequestHeader and VisitHeaders. The reason is that we don't // want various headers like Link: and Refresh: applying to view-source. + +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) { diff --git a/toolkit/components/passwordmgr/nsLoginManagerPrompter.js b/toolkit/components/passwordmgr/nsLoginManagerPrompter.js index eb534443e..e5f0b8407 100644 --- a/toolkit/components/passwordmgr/nsLoginManagerPrompter.js +++ b/toolkit/components/passwordmgr/nsLoginManagerPrompter.js @@ -95,30 +95,64 @@ LoginManagerPromptFactory.prototype = { return; } - this._asyncPromptInProgress = true; - prompt.inProgress = true; + // Set up a counter for ensuring that the basic auth prompt can not + // be abused for DOS-style attacks. With this counter, each eTLD+1 + // per browser will get a limited number of times a user can + // cancel the prompt until we stop showing it. + let browser = prompter._browser; + let baseDomain = null; + if (browser) { + try { + baseDomain = Services.eTLD.getBaseDomainFromHost(hostname); + } catch (e) { + baseDomain = hostname; + } + + if (!browser.canceledAuthenticationPromptCounter) { + browser.canceledAuthenticationPromptCounter = {}; + } + + if (!browser.canceledAuthenticationPromptCounter[baseDomain]) { + browser.canceledAuthenticationPromptCounter[baseDomain] = 0; + } + } 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"); - } catch (e) { - Components.utils.reportError("LoginManagerPrompter: " + - "_doAsyncPrompt:run: " + e + "\n"); - } + 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; + delete self._asyncPrompts[hashKey]; + prompt.inProgress = false; + self._asyncPromptInProgress = false; + + if (browser) { + // Reset the counter state if the user replied to a prompt and actually + // tried to login (vs. simply clicking any button to get out). + if (ok && (prompt.authInfo.username || prompt.authInfo.password)) { + browser.canceledAuthenticationPromptCounter[baseDomain] = 0; + } else { + browser.canceledAuthenticationPromptCounter[baseDomain] += 1; + } + } + } for (var consumer of prompt.consumers) { if (!consumer.callback) @@ -128,16 +162,37 @@ LoginManagerPromptFactory.prototype = { 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"); + + if (browser) { + let cancelationCounter = browser.canceledAuthenticationPromptCounter[baseDomain]; + this.log("cancelationCounter ="+ cancelationCounter); + if (cancelDialogLimit && cancelationCounter >= 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; + } + } else { + this._asyncPromptInProgress = true; + prompt.inProgress = true; + } + Services.tm.mainThread.dispatch(runnable, Ci.nsIThread.DISPATCH_NORMAL); this.log("_doAsyncPrompt:run dispatched"); },