Skip to content

Commit

Permalink
Bug 1314912 - Rate limit calls to History and Location interfaces. r=…
Browse files Browse the repository at this point in the history
…smaug

This adds a rate limit to methods and setters of the History and Location
for non-system callers.
The rate limit is counted per BrowsingContext and can be controlled by prefs.

This patch is based on the original rate limit patch by :freesamael.

Differential Revision: https://phabricator.services.mozilla.com/D90136
  • Loading branch information
Trikolon committed Sep 21, 2020
1 parent 9220090 commit 3c65472
Show file tree
Hide file tree
Showing 11 changed files with 141 additions and 22 deletions.
50 changes: 50 additions & 0 deletions docshell/base/BrowsingContext.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2753,6 +2753,56 @@ bool BrowsingContext::ShouldUpdateSessionHistory(uint32_t aLoadType) {
(IsForceReloadType(aLoadType) && IsFrame()));
}

nsresult BrowsingContext::CheckLocationChangeRateLimit(CallerType aCallerType) {
// We only rate limit non system callers
if (aCallerType == CallerType::System) {
return NS_OK;
}

// Fetch rate limiting preferences
uint32_t limitCount =
StaticPrefs::dom_navigation_locationChangeRateLimit_count();
uint32_t timeSpanSeconds =
StaticPrefs::dom_navigation_locationChangeRateLimit_timespan();

// Disable throttling if either of the preferences is set to 0.
if (limitCount == 0 || timeSpanSeconds == 0) {
return NS_OK;
}

TimeDuration throttleSpan = TimeDuration::FromSeconds(timeSpanSeconds);

if (mLocationChangeRateLimitSpanStart.IsNull() ||
((TimeStamp::Now() - mLocationChangeRateLimitSpanStart) > throttleSpan)) {
// Initial call or timespan exceeded, reset counter and timespan.
mLocationChangeRateLimitSpanStart = TimeStamp::Now();
mLocationChangeRateLimitCount = 1;
return NS_OK;
}

if (mLocationChangeRateLimitCount >= limitCount) {
// Rate limit reached

Document* doc = GetDocument();
if (doc) {
nsContentUtils::ReportToConsole(nsIScriptError::errorFlag, "DOM"_ns, doc,
nsContentUtils::eDOM_PROPERTIES,
"LocChangeFloodingPrevented");
}

return NS_ERROR_DOM_SECURITY_ERR;
}

mLocationChangeRateLimitCount++;
return NS_OK;
}

void BrowsingContext::ResetLocationChangeRateLimit() {
// Resetting the timestamp object will cause the check function to
// init again and reset the rate limit.
mLocationChangeRateLimitSpanStart = TimeStamp();
}

} // namespace dom

namespace ipc {
Expand Down
15 changes: 15 additions & 0 deletions docshell/base/BrowsingContext.h
Original file line number Diff line number Diff line change
Expand Up @@ -690,6 +690,16 @@ class BrowsingContext : public nsILoadContext, public nsWrapperCache {

bool ShouldUpdateSessionHistory(uint32_t aLoadType);

// Checks if we reached the rate limit for calls to Location and History API.
// The rate limit is controlled by the
// "dom.navigation.locationChangeRateLimit" prefs.
// Rate limit applies per BrowsingContext.
// Returns NS_OK if we are below the rate limit and increments the counter.
// Returns NS_ERROR_DOM_SECURITY_ERR if limit is reached.
nsresult CheckLocationChangeRateLimit(CallerType aCallerType);

void ResetLocationChangeRateLimit();

protected:
virtual ~BrowsingContext();
BrowsingContext(WindowContext* aParentWindow, BrowsingContextGroup* aGroup,
Expand Down Expand Up @@ -989,6 +999,11 @@ class BrowsingContext : public nsILoadContext, public nsWrapperCache {

RefPtr<SessionStorageManager> mSessionStorageManager;
RefPtr<ChildSHistory> mChildSessionHistory;

// Counter and time span for rate limiting Location and History API calls.
// Used by CheckLocationChangeRateLimit. Do not apply cross-process.
uint32_t mLocationChangeRateLimitCount;
mozilla::TimeStamp mLocationChangeRateLimitSpanStart;
};

/**
Expand Down
9 changes: 8 additions & 1 deletion docshell/shistory/ChildSHistory.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,14 @@ void ChildSHistory::Go(int32_t aOffset, bool aRequireUserInteraction,
GotoIndex(index.value(), aRv);
}

void ChildSHistory::AsyncGo(int32_t aOffset, bool aRequireUserInteraction) {
void ChildSHistory::AsyncGo(int32_t aOffset, bool aRequireUserInteraction,
CallerType aCallerType, ErrorResult& aRv) {
nsresult rv = mBrowsingContext->CheckLocationChangeRateLimit(aCallerType);
if (NS_FAILED(rv)) {
aRv.Throw(rv);
return;
}

if (!CanGo(aOffset)) {
return;
}
Expand Down
3 changes: 2 additions & 1 deletion docshell/shistory/ChildSHistory.h
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ class ChildSHistory : public nsISupports, public nsWrapperCache {
*/
bool CanGo(int32_t aOffset);
void Go(int32_t aOffset, bool aRequireUserInteraction, ErrorResult& aRv);
void AsyncGo(int32_t aOffset, bool aRequireUserInteraction);
void AsyncGo(int32_t aOffset, bool aRequireUserInteraction,
CallerType aCallerType, ErrorResult& aRv);

void GotoIndex(int32_t aIndex, ErrorResult& aRv);

Expand Down
12 changes: 11 additions & 1 deletion dom/base/LocationBase.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,16 @@ void LocationBase::SetURI(nsIURI* aURI, nsIPrincipal& aSubjectPrincipal,
return;
}

CallerType callerType = aSubjectPrincipal.IsSystemPrincipal()
? CallerType::System
: CallerType::NonSystem;

nsresult rv = bc->CheckLocationChangeRateLimit(callerType);
if (NS_FAILED(rv)) {
aRv.Throw(rv);
return;
}

RefPtr<nsDocShellLoadState> loadState =
CheckURL(aURI, aSubjectPrincipal, aRv);
if (aRv.Failed()) {
Expand All @@ -143,7 +153,7 @@ void LocationBase::SetURI(nsIURI* aURI, nsIPrincipal& aSubjectPrincipal,
loadState->SetLoadFlags(nsIWebNavigation::LOAD_FLAGS_NONE);
loadState->SetFirstParty(true);

nsresult rv = bc->LoadURI(loadState);
rv = bc->LoadURI(loadState);
if (NS_WARN_IF(NS_FAILED(rv))) {
aRv.Throw(rv);
}
Expand Down
36 changes: 25 additions & 11 deletions dom/base/nsHistory.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ void nsHistory::GetState(JSContext* aCx, JS::MutableHandle<JS::Value> aResult,
aResult.setNull();
}

void nsHistory::Go(int32_t aDelta, ErrorResult& aRv) {
void nsHistory::Go(int32_t aDelta, CallerType aCallerType, ErrorResult& aRv) {
nsCOMPtr<nsPIDOMWindowInner> win(do_QueryReferent(mInnerWindow));
if (!win || !win->HasActiveDocument()) {
return aRv.Throw(NS_ERROR_DOM_SECURITY_ERR);
Expand All @@ -154,15 +154,17 @@ void nsHistory::Go(int32_t aDelta, ErrorResult& aRv) {

// Ignore the return value from Go(), since returning errors from Go() can
// lead to exceptions and a possible leak of history length
// AsyncGo throws if we hit the location change rate limit.
if (StaticPrefs::dom_window_history_async()) {
session_history->AsyncGo(aDelta, /* aRequireUserInteraction = */ false);
session_history->AsyncGo(aDelta, /* aRequireUserInteraction = */ false,
aCallerType, aRv);
} else {
session_history->Go(aDelta, /* aRequireUserInteraction = */ false,
IgnoreErrors());
}
}

void nsHistory::Back(ErrorResult& aRv) {
void nsHistory::Back(CallerType aCallerType, ErrorResult& aRv) {
nsCOMPtr<nsPIDOMWindowInner> win(do_QueryReferent(mInnerWindow));
if (!win || !win->HasActiveDocument()) {
aRv.Throw(NS_ERROR_DOM_SECURITY_ERR);
Expand All @@ -178,13 +180,14 @@ void nsHistory::Back(ErrorResult& aRv) {
}

if (StaticPrefs::dom_window_history_async()) {
sHistory->AsyncGo(-1, /* aRequireUserInteraction = */ false);
sHistory->AsyncGo(-1, /* aRequireUserInteraction = */ false, aCallerType,
aRv);
} else {
sHistory->Go(-1, /* aRequireUserInteraction = */ false, IgnoreErrors());
}
}

void nsHistory::Forward(ErrorResult& aRv) {
void nsHistory::Forward(CallerType aCallerType, ErrorResult& aRv) {
nsCOMPtr<nsPIDOMWindowInner> win(do_QueryReferent(mInnerWindow));
if (!win || !win->HasActiveDocument()) {
aRv.Throw(NS_ERROR_DOM_SECURITY_ERR);
Expand All @@ -200,27 +203,29 @@ void nsHistory::Forward(ErrorResult& aRv) {
}

if (StaticPrefs::dom_window_history_async()) {
sHistory->AsyncGo(1, /* aRequireUserInteraction = */ false);
sHistory->AsyncGo(1, /* aRequireUserInteraction = */ false, aCallerType,
aRv);
} else {
sHistory->Go(1, /* aRequireUserInteraction = */ false, IgnoreErrors());
}
}

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);
CallerType aCallerType, ErrorResult& aRv) {
PushOrReplaceState(aCx, aData, aTitle, aUrl, aCallerType, aRv, false);
}

void nsHistory::ReplaceState(JSContext* aCx, JS::Handle<JS::Value> aData,
const nsAString& aTitle, const nsAString& aUrl,
ErrorResult& aRv) {
PushOrReplaceState(aCx, aData, aTitle, aUrl, aRv, true);
CallerType aCallerType, ErrorResult& aRv) {
PushOrReplaceState(aCx, aData, aTitle, aUrl, aCallerType, aRv, true);
}

void nsHistory::PushOrReplaceState(JSContext* aCx, JS::Handle<JS::Value> aData,
const nsAString& aTitle,
const nsAString& aUrl, ErrorResult& aRv,
const nsAString& aUrl,
CallerType aCallerType, ErrorResult& aRv,
bool aReplace) {
nsCOMPtr<nsPIDOMWindowInner> win(do_QueryReferent(mInnerWindow));
if (!win) {
Expand All @@ -235,6 +240,15 @@ void nsHistory::PushOrReplaceState(JSContext* aCx, JS::Handle<JS::Value> aData,
return;
}

BrowsingContext* bc = win->GetBrowsingContext();
if (bc) {
nsresult rv = bc->CheckLocationChangeRateLimit(aCallerType);
if (NS_FAILED(rv)) {
aRv.Throw(rv);
return;
}
}

// AddState might run scripts, so we need to hold a strong reference to the
// docShell here to keep it from going away.
nsCOMPtr<nsIDocShell> docShell = win->GetDocShell();
Expand Down
10 changes: 7 additions & 3 deletions dom/base/nsHistory.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,21 +42,25 @@ class nsHistory final : public nsISupports, public nsWrapperCache {
mozilla::ErrorResult& aRv);
void GetState(JSContext* aCx, JS::MutableHandle<JS::Value> aResult,
mozilla::ErrorResult& aRv) const;
void Go(int32_t aDelta, mozilla::ErrorResult& aRv);
void Back(mozilla::ErrorResult& aRv);
void Forward(mozilla::ErrorResult& aRv);
void Go(int32_t aDelta, mozilla::dom::CallerType aCallerType,
mozilla::ErrorResult& aRv);
void Back(mozilla::dom::CallerType aCallerType, mozilla::ErrorResult& aRv);
void Forward(mozilla::dom::CallerType aCallerType, mozilla::ErrorResult& aRv);
void PushState(JSContext* aCx, JS::Handle<JS::Value> aData,
const nsAString& aTitle, const nsAString& aUrl,
mozilla::dom::CallerType aCallerType,
mozilla::ErrorResult& aRv);
void ReplaceState(JSContext* aCx, JS::Handle<JS::Value> aData,
const nsAString& aTitle, const nsAString& aUrl,
mozilla::dom::CallerType aCallerType,
mozilla::ErrorResult& aRv);

protected:
virtual ~nsHistory();

void PushOrReplaceState(JSContext* aCx, JS::Handle<JS::Value> aData,
const nsAString& aTitle, const nsAString& aUrl,
mozilla::dom::CallerType aCallerType,
mozilla::ErrorResult& aRv, bool aReplace);

already_AddRefed<mozilla::dom::ChildSHistory> GetSessionHistory() const;
Expand Down
3 changes: 3 additions & 0 deletions dom/chrome-webidl/BrowsingContext.webidl
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,9 @@ interface BrowsingContext {
[SetterThrows] attribute unsigned long long browserId;

readonly attribute ChildSHistory? childSessionHistory;

// Resets the location change rate limit. Used for testing.
void resetLocationChangeRateLimit();
};

BrowsingContext includes LoadContextMixin;
Expand Down
2 changes: 2 additions & 0 deletions dom/locales/en-US/chrome/dom/dom.properties
Original file line number Diff line number Diff line change
Expand Up @@ -395,3 +395,5 @@ RequestStorageAccessSandboxed=document.requestStorageAccess() may not be called
RequestStorageAccessNested=document.requestStorageAccess() may not be called in a nested iframe.
# LOCALIZATION NOTE: Do not translate document.requestStorageAccess(). In some locales it may be preferable to not translate "event handler", either.
RequestStorageAccessUserGesture=document.requestStorageAccess() may only be requested from inside a short running user-generated event handler.
# LOCALIZATION NOTE: Do not translate "Location" and "History".
LocChangeFloodingPrevented=Too many calls to Location or History APIs within a short timeframe.
10 changes: 5 additions & 5 deletions dom/webidl/History.webidl
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,14 @@ interface History {
attribute ScrollRestoration scrollRestoration;
[Throws]
readonly attribute any state;
[Throws]
[Throws, NeedsCallerType]
void go(optional long delta = 0);
[Throws]
[Throws, NeedsCallerType]
void back();
[Throws]
[Throws, NeedsCallerType]
void forward();
[Throws]
[Throws, NeedsCallerType]
void pushState(any data, DOMString title, optional DOMString? url = null);
[Throws]
[Throws, NeedsCallerType]
void replaceState(any data, DOMString title, optional DOMString? url = null);
};
13 changes: 13 additions & 0 deletions modules/libpref/init/StaticPrefList.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2193,6 +2193,19 @@
value: true
mirror: always

# Limit of location change caused by content scripts in a time span per
# BrowsingContext. This includes calls to History and Location APIs.
- name: dom.navigation.locationChangeRateLimit.count
type: uint32_t
value: 200
mirror: always

# Time span in seconds for location change rate limit.
- name: dom.navigation.locationChangeRateLimit.timespan
type: uint32_t
value: 10
mirror: always

# Network Information API
- name: dom.netinfo.enabled
type: RelaxedAtomicBool
Expand Down

0 comments on commit 3c65472

Please sign in to comment.