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 draft
authorSamael Wang <freesamael@gmail.com>
Thu, 07 Dec 2017 14:15:15 +0800
changeset 708781 4ad2959899240b97b38a78b6b6371dced7a0d5c6
parent 704807 cb9092a90f6ef501e6de8eb5fc6ce19e2717193f
child 708782 fded9e6185cd0317d7b7d4ee9a894edc7ea8d619
child 708810 f98c381819a9fb6314b557f806e48462400743be
push id92449
push userbmo:sawang@mozilla.com
push dateThu, 07 Dec 2017 06:37:05 +0000
reviewersbz
bugs1314912
milestone59.0a1
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
docshell/base/nsDocShell.cpp
docshell/base/nsDocShell.h
docshell/shistory/nsSHistory.cpp
dom/base/nsHistory.cpp
dom/locales/en-US/chrome/dom/dom.properties
--- 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.