Bug 1314912 - Part 1: Throttle same document navigation caused by content scripts, if there have been too many LOCATION_CHANGE_SAME_DOCUMENT caused by content scripts in a short time frame. r?bz
The patch records the number of successive LOCATION_CHANGE_SAME_DOCUMENT when
the incumbent global exists and it's not system principal, which should
basically indicate content scripts are using History or Location APIs.
When the number of successive LOCATION_CHANGE_SAME_DOCUMENT recorded exceeds
the limit, it throws in InternalLoad and AddState if the caller is from content
scripts and it's making same document navigation.
MozReview-Commit-ID: 7NDp4eOjesp
--- a/docshell/base/nsDocShell.cpp
+++ b/docshell/base/nsDocShell.cpp
@@ -255,16 +255,18 @@ using namespace mozilla;
using namespace mozilla::dom;
using mozilla::dom::workers::ServiceWorkerManager;
// True means sUseErrorPages has been added to
// preferences var cache.
static bool gAddedPreferencesVarCache = false;
bool nsDocShell::sUseErrorPages = false;
+int32_t nsDocShell::sSameDocNavLimit = 0;
+int32_t nsDocShell::sSameDocNavThrottleSpan = 0;
// Number of documents currently loading
static int32_t gNumberOfDocumentsLoading = 0;
// Global count of existing docshells.
static int32_t gDocShellCount = 0;
// Global count of docshells with the private attribute set
@@ -788,16 +790,17 @@ nsDocShell::nsDocShell()
, mMarginWidth(-1)
, mMarginHeight(-1)
, mItemType(typeContent)
, mPreviousTransIndex(-1)
, mLoadedTransIndex(-1)
, mSandboxFlags(0)
, mOrientationLock(eScreenOrientation_None)
, mFullscreenAllowed(CHECK_ATTRIBUTES)
+ , mSameDocLocChangeCount(0)
, mCreated(false)
, mAllowSubframes(true)
, mAllowPlugins(true)
, mAllowJavascript(true)
, mAllowMetaRedirects(true)
, mAllowImages(true)
, mAllowMedia(true)
, mAllowDNSPrefetch(true)
@@ -1820,16 +1823,69 @@ nsDocShell::DispatchLocationChangeEvent(
{
return DispatchToTabGroup(
TaskCategory::Other,
NewRunnableMethod("nsDocShell::FireDummyOnLocationChange",
this,
&nsDocShell::FireDummyOnLocationChange));
}
+void
+nsDocShell::RecordAndFireOnLocationChange(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest,
+ nsIURI *aUri,
+ uint32_t aFlags)
+{
+ if (aFlags & LOCATION_CHANGE_SAME_DOCUMENT) {
+ // We're only interested in LOCATION_CHANGE_SAME_DOCUMENT caused by content
+ // scripts.
+ nsCOMPtr<nsIGlobalObject> incumbent = GetIncumbentGlobal();
+ nsIPrincipal* callerPrincipal;
+ if (incumbent &&
+ (callerPrincipal = incumbent->PrincipalOrNull()) &&
+ !nsContentUtils::IsSystemPrincipal(callerPrincipal)) {
+ if (mSameDocLocChangeCount == 0) {
+ mSameDocNavThrottleSpanStart = TimeStamp::Now();
+ }
+ mSameDocLocChangeCount++;
+ }
+ } else {
+ mSameDocLocChangeCount = 0;
+ }
+
+ FireOnLocationChange(aWebProgress, aRequest, aUri, aFlags);
+}
+
+bool
+nsDocShell::ShouldThrottleSameDocNav()
+{
+ // Only throttle on content scripts.
+ nsCOMPtr<nsIGlobalObject> incumbent = GetIncumbentGlobal();
+ if (!incumbent ||
+ nsContentUtils::IsSystemPrincipal(incumbent->PrincipalOrNull())) {
+ return false;
+ }
+
+ // If either of the preferences is set to non-positive value then disable
+ // throttling.
+ if (sSameDocNavLimit <= 0 || sSameDocNavThrottleSpan <= 0) {
+ return false;
+ }
+
+ TimeDuration throttleSpan =
+ TimeDuration::FromSeconds(sSameDocNavThrottleSpan);
+ if (mSameDocNavThrottleSpanStart.IsNull() ||
+ (TimeStamp::Now() - mSameDocNavThrottleSpanStart > throttleSpan)) {
+ mSameDocLocChangeCount = 0;
+ return false;
+ }
+
+ return mSameDocLocChangeCount >= sSameDocNavLimit;
+}
+
bool
nsDocShell::MaybeInitTiming()
{
if (mTiming && !mBlankTiming) {
return false;
}
bool canBeReset = false;
@@ -2069,17 +2125,17 @@ nsDocShell::SetCurrentURI(nsIURI* aURI,
* We don't want to send OnLocationChange notifications when
* a subframe is being loaded for the first time, while
* visiting a frameset page
*/
return false;
}
if (aFireOnLocationChange) {
- FireOnLocationChange(this, aRequest, aURI, aLocationFlags);
+ RecordAndFireOnLocationChange(this, aRequest, aURI, aLocationFlags);
}
return !aFireOnLocationChange;
}
NS_IMETHODIMP
nsDocShell::GetCharset(nsACString& aCharset)
{
aCharset.Truncate();
@@ -5953,16 +6009,22 @@ nsDocShell::Create()
// Should we use XUL error pages instead of alerts if possible?
mUseErrorPages =
Preferences::GetBool("browser.xul.error_pages.enabled", mUseErrorPages);
if (!gAddedPreferencesVarCache) {
Preferences::AddBoolVarCache(&sUseErrorPages,
"browser.xul.error_pages.enabled",
mUseErrorPages);
+ Preferences::AddIntVarCache(&sSameDocNavLimit,
+ "dom.navigation.same_doc.limit",
+ 100);
+ Preferences::AddIntVarCache(&sSameDocNavThrottleSpan,
+ "dom.navigation.same_doc.limit.timespan",
+ 10);
gAddedPreferencesVarCache = true;
}
mDisableMetaRefreshWhenInactive =
Preferences::GetBool("browser.meta_refresh_when_inactive.disabled",
mDisableMetaRefreshWhenInactive);
mDeviceSizeIsPageSize =
@@ -9400,18 +9462,18 @@ nsDocShell::CreateContentViewer(const ns
// Create an shistory entry for the old load.
if (failedURI) {
bool errorOnLocationChangeNeeded = OnNewURI(
failedURI, failedChannel, triggeringPrincipal,
nullptr, mLoadType, false, false, false);
if (errorOnLocationChangeNeeded) {
- FireOnLocationChange(this, failedChannel, failedURI,
- LOCATION_CHANGE_ERROR_PAGE);
+ RecordAndFireOnLocationChange(this, failedChannel, failedURI,
+ LOCATION_CHANGE_ERROR_PAGE);
}
}
// Be sure to have a correct mLSHE, it may have been cleared by
// EndPageLoad. See bug 302115.
if (mSessionHistory && !mLSHE) {
int32_t idx;
mSessionHistory->GetRequestedIndex(&idx);
@@ -9494,17 +9556,17 @@ nsDocShell::CreateContentViewer(const ns
if (++gNumberOfDocumentsLoading == 1) {
// Hint to favor performance for the plevent notification mechanism.
// We want the pages to load as fast as possible even if its means
// native messages might be starved.
FavorPerformanceHint(true);
}
if (onLocationChangeNeeded) {
- FireOnLocationChange(this, aRequest, mCurrentURI, 0);
+ RecordAndFireOnLocationChange(this, aRequest, mCurrentURI, 0);
}
return NS_OK;
}
nsresult
nsDocShell::NewContentViewerObj(const nsACString& aContentType,
nsIRequest* aRequest, nsILoadGroup* aLoadGroup,
@@ -10567,16 +10629,25 @@ nsDocShell::InternalLoad(nsIURI* aURI,
// that history.go(0) and the like trigger full refreshes, rather than
// short-circuited loads.
bool doShortCircuitedLoad =
(historyNavBetweenSameDoc && mOSHE != aSHEntry) ||
(!aSHEntry && !aPostData &&
sameExceptHashes && newURIHasRef);
if (doShortCircuitedLoad) {
+ if (ShouldThrottleSameDocNav()) {
+ nsContentUtils::ReportToConsole(nsIScriptError::exceptionFlag,
+ NS_LITERAL_CSTRING("DOM"),
+ GetDocument(),
+ nsContentUtils::eDOM_PROPERTIES,
+ "LocChangeFloodingPrevented");
+ return NS_ERROR_DOM_SECURITY_ERR;
+ }
+
// Save the position of the scrollers.
nscoord cx = 0, cy = 0;
GetCurScrollPos(ScrollOrientation_X, &cx);
GetCurScrollPos(ScrollOrientation_Y, &cy);
// Reset mLoadType to its original value once we exit this block,
// because this short-circuited load might have started after a
// normal, network load, and we don't want to clobber its load type.
@@ -12320,16 +12391,25 @@ nsDocShell::AddState(JS::Handle<JS::Valu
// changes to the hash at this stage of the game.
if (JustStartedNetworkLoad()) {
aReplace = true;
}
nsCOMPtr<nsIDocument> document = GetDocument();
NS_ENSURE_TRUE(document, NS_ERROR_FAILURE);
+ if (ShouldThrottleSameDocNav()) {
+ nsContentUtils::ReportToConsole(nsIScriptError::exceptionFlag,
+ NS_LITERAL_CSTRING("DOM"),
+ document,
+ nsContentUtils::eDOM_PROPERTIES,
+ "LocChangeFloodingPrevented");
+ return NS_ERROR_DOM_SECURITY_ERR;
+ }
+
// Step 1: Serialize aData using structured clone.
nsCOMPtr<nsIStructuredCloneContainer> scContainer;
// scContainer->Init might cause arbitrary JS to run, and this code might
// navigate the page we're on, potentially to a different origin! (bug
// 634834) To protect against this, we abort if our principal changes due
// to the InitFromJSVal() call.
{
--- a/docshell/base/nsDocShell.h
+++ b/docshell/base/nsDocShell.h
@@ -268,18 +268,18 @@ public:
friend class OnLinkClickEvent;
static bool SandboxFlagsImplyCookies(const uint32_t &aSandboxFlags);
// We need dummy OnLocationChange in some cases to update the UI without
// updating security info.
void FireDummyOnLocationChange()
{
- FireOnLocationChange(this, nullptr, mCurrentURI,
- LOCATION_CHANGE_SAME_DOCUMENT);
+ RecordAndFireOnLocationChange(this, nullptr, mCurrentURI,
+ LOCATION_CHANGE_SAME_DOCUMENT);
}
nsresult HistoryTransactionRemoved(int32_t aIndex);
// Notify Scroll observers when an async panning/zooming transform
// has started being applied
void NotifyAsyncPanZoomStarted();
// Notify Scroll observers when an async panning/zooming transform
@@ -1044,19 +1044,33 @@ protected:
enum FullscreenAllowedState : uint8_t
{
CHECK_ATTRIBUTES,
PARENT_ALLOWS,
PARENT_PROHIBITS
};
FullscreenAllowedState mFullscreenAllowed;
+ // The number of successive LOCATION_CHANGE_SAME_DOCUMENT caused by content
+ // scripts since mSameDocNavThrottleSpanStart.
+ int32_t mSameDocLocChangeCount;
+ mozilla::TimeStamp mSameDocNavThrottleSpanStart;
+
// Cached value of the "browser.xul.error_pages.enabled" preference.
static bool sUseErrorPages;
+ // Cached value of the "dom.navigation.same_doc.limit" preference, which
+ // controls the limit of same document navigation caused by content scripts in
+ // a time span.
+ static int32_t sSameDocNavLimit;
+
+ // Cached value of the "dom.navigation.same_doc.limit.timespan" preference,
+ // which is the time span for sSameDocNavLimit in seconds.
+ static int32_t sSameDocNavThrottleSpan;
+
bool mCreated : 1;
bool mAllowSubframes : 1;
bool mAllowPlugins : 1;
bool mAllowJavascript : 1;
bool mAllowMetaRedirects : 1;
bool mAllowImages : 1;
bool mAllowMedia : 1;
bool mAllowDNSPrefetch : 1;
@@ -1210,16 +1224,31 @@ private:
// children docshells.
void FirePageHideNotificationInternal(bool aIsUnload,
bool aSkipCheckingDynEntries);
// Dispatch a runnable to the TabGroup associated to this docshell.
nsresult DispatchToTabGroup(mozilla::TaskCategory aCategory,
already_AddRefed<nsIRunnable>&& aRunnable);
+ // Record the number of successive LOCATION_CHANGE_SAME_DOCUMENT and
+ // fire the location change event through nsDocLoader.
+ //
+ // We don't want to make nsDocLoader::FireOnLocationChange be virtual and
+ // override it, since nsDocLoader would propagate the call to all parents'
+ // FireOnLocationChange but we only want to record it on the source docshell.
+ void RecordAndFireOnLocationChange(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest,
+ nsIURI *aUri,
+ uint32_t aFlags);
+
+ // Check if we should throttle same document navigation based on the number
+ // of successive LOCATION_CHANGE_SAME_DOCUMENT caused by content scripts.
+ bool ShouldThrottleSameDocNav();
+
#ifdef DEBUG
// We're counting the number of |nsDocShells| to help find leaks
static unsigned long gNumberOfDocShells;
#endif /* DEBUG */
public:
class InterfaceRequestorProxy : public nsIInterfaceRequestor
{
--- a/docshell/shistory/nsSHistory.cpp
+++ b/docshell/shistory/nsSHistory.cpp
@@ -1934,17 +1934,17 @@ nsSHistory::LoadDifferingEntries(nsISHEn
break;
}
}
}
// Finally recursively call this method.
// This will either load a new page to shell or some subshell or
// do nothing.
- LoadDifferingEntries(pChild, nChild, dsChild, aLoadType, aDifferenceFound);
+ result = LoadDifferingEntries(pChild, nChild, dsChild, aLoadType, aDifferenceFound);
}
return result;
}
nsresult
nsSHistory::InitiateLoad(nsISHEntry* aFrameEntry, nsIDocShell* aFrameDS,
long aLoadType)
{
--- a/dom/base/nsHistory.cpp
+++ b/dom/base/nsHistory.cpp
@@ -209,18 +209,22 @@ nsHistory::Go(int32_t aDelta, ErrorResul
}
int32_t curIndex = -1;
int32_t len = 0;
session_history->GetGlobalIndex(&curIndex);
session_history->GetGlobalCount(&len);
int32_t index = curIndex + aDelta;
- if (index > -1 && index < len)
- webnav->GotoIndex(index);
+ if (index > -1 && index < len) {
+ nsresult rv = webnav->GotoIndex(index);
+ if (rv == NS_ERROR_DOM_SECURITY_ERR) {
+ aRv.Throw(NS_ERROR_DOM_SECURITY_ERR);
+ }
+ }
// Ignore the return value from GotoIndex(), since returning errors
// from GotoIndex() can lead to exceptions and a possible leak
// of history length
}
void
nsHistory::Back(ErrorResult& aRv)
@@ -235,17 +239,20 @@ nsHistory::Back(ErrorResult& aRv)
nsCOMPtr<nsISHistory> sHistory = GetSessionHistory();
nsCOMPtr<nsIWebNavigation> webNav(do_QueryInterface(sHistory));
if (!webNav) {
aRv.Throw(NS_ERROR_FAILURE);
return;
}
- webNav->GoBack();
+ nsresult rv = webNav->GoBack();
+ if (rv == NS_ERROR_DOM_SECURITY_ERR) {
+ aRv.Throw(NS_ERROR_DOM_SECURITY_ERR);
+ }
}
void
nsHistory::Forward(ErrorResult& aRv)
{
nsCOMPtr<nsPIDOMWindowInner> win(do_QueryReferent(mInnerWindow));
if (!win || !win->HasActiveDocument()) {
aRv.Throw(NS_ERROR_DOM_SECURITY_ERR);
@@ -256,17 +263,20 @@ nsHistory::Forward(ErrorResult& aRv)
nsCOMPtr<nsISHistory> sHistory = GetSessionHistory();
nsCOMPtr<nsIWebNavigation> webNav(do_QueryInterface(sHistory));
if (!webNav) {
aRv.Throw(NS_ERROR_FAILURE);
return;
}
- webNav->GoForward();
+ nsresult rv = webNav->GoForward();
+ if (rv == NS_ERROR_DOM_SECURITY_ERR) {
+ aRv.Throw(NS_ERROR_DOM_SECURITY_ERR);
+ }
}
void
nsHistory::PushState(JSContext* aCx, JS::Handle<JS::Value> aData,
const nsAString& aTitle, const nsAString& aUrl,
ErrorResult& aRv)
{
PushOrReplaceState(aCx, aData, aTitle, aUrl, aRv, false);
--- a/dom/locales/en-US/chrome/dom/dom.properties
+++ b/dom/locales/en-US/chrome/dom/dom.properties
@@ -334,8 +334,10 @@ ScriptSourceLoadFailed=Loading failed for the <script> with source “%S”.
# LOCALIZATION NOTE: Do not translate "<script>".
ScriptSourceMalformed=<script> source URI is malformed: “%S”.
# LOCALIZATION NOTE: Do not translate "<script>".
ScriptSourceNotAllowed=<script> source URI is not allowed in this document: “%S”.
# LOCALIZATION NOTE: %1$S is the invalid property value and %2$S is the property name.
InvalidKeyframePropertyValue=Keyframe property value “%1$S” is invalid according to the syntax for “%2$S”.
# LOCALIZATION NOTE: Do not translate "ReadableStream".
ReadableStreamReadingFailed=Failed to read data from the ReadableStream: “%S”.
+# LOCALIZATION NOTE: Do not translate "Location" and "History".
+LocChangeFloodingPrevented=Excessive calls to Location or History APIs within a short timeframe.