#547: block modal HTTP auth DOS M1312243 M377496 + glue code

This commit is contained in:
Cameron Kaiser 2019-03-26 22:23:40 -07:00
parent 9465ea36d8
commit d60d9721fd
15 changed files with 207 additions and 23 deletions

View File

@ -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);

View File

@ -2679,6 +2679,7 @@ let remote = false;
<parameter name="aTab"/>
<body>
<![CDATA[
delete this.getBrowserForTab(aTab).canceledAuthenticationPromptCounter;
this.getBrowserForTab(aTab).reload();
]]>
</body>

View File

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

View File

@ -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<Element> frameElement = mScriptGlobal->GetFrameElementInternal();
if (MOZ_LIKELY(frameElement)) {
nsCOMPtr<nsIContent> frame = do_QueryInterface(frameElement);
if (MOZ_LIKELY(frame)) {
nsCOMPtr<nsISupports> prompt;
nsCOMPtr<nsIDOMWindow> window = do_QueryInterface(frame->OwnerDoc()->GetWindow());
if (MOZ_LIKELY(window) &&
NS_SUCCEEDED(wwatch->GetPrompt(window, aIID,
getter_AddRefs(prompt))) &&
MOZ_LIKELY(prompt)) {
nsCOMPtr<nsIDOMElement> browser = do_QueryInterface(frameElement);
if (MOZ_LIKELY(browser)) {
nsCOMPtr<nsILoginManagerPrompter> 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<void**>(aResult));

View File

@ -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);

View File

@ -117,6 +117,7 @@ struct HttpChannelOpenArgs
OptionalCorsPreflightArgs preflightArgs;
uint32_t initialRwin;
bool suspendAfterSynthesizeResponse;
uint64_t contentWindowId;
};
struct HttpChannelConnectArgs

View File

@ -48,8 +48,10 @@
#include "LoadInfo.h"
#include "nsIXULRuntime.h"
#include "nsPIDOMWindow.h"
#include "nsIDOMWindowUtils.h"
#include <algorithm>
#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<nsILoadContext> loadContext;
GetCallback(loadContext);
if (loadContext) {
nsCOMPtr<nsIDOMWindow> 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)
{

View File

@ -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<nsCString> mUnsafeHeaders;

View File

@ -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<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.
@ -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();

View File

@ -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<nsIURI> uri = DeserializeURI(aURI);
if (!uri) {
@ -440,6 +442,7 @@ HttpChannelParent::DoAsyncOpen( const URIParams& aURI,
return SendFailedAsyncOpen(rv);
mChannel = static_cast<nsHttpChannel *>(channel.get());
mChannel->SetTopLevelContentWindowId(aContentWindowId);
mChannel->SetWarningReporter(this);
mChannel->SetTimingEnabled(true);
if (mPBOverride != kPBOverride_Unset) {

View File

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

View File

@ -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)
{

View File

@ -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;
};

View File

@ -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)
{

View File

@ -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");
},