Bug 1314912 - Rate limit calls to History and Location interfaces. r=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
This commit is contained in:
pbz 2020-09-21 11:54:50 +00:00
parent 270e7e2200
commit b81e69ee95
11 changed files with 141 additions and 22 deletions

View File

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

View File

@ -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,
@ -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;
};
/**

View File

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

View File

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

View File

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

View File

@ -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);
@ -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);
@ -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);
@ -200,7 +203,8 @@ 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());
}
@ -208,19 +212,20 @@ void nsHistory::Forward(ErrorResult& aRv) {
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) {
@ -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();

View File

@ -42,14 +42,17 @@ 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:
@ -57,6 +60,7 @@ class nsHistory final : public nsISupports, public nsWrapperCache {
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;

View File

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

View File

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

View File

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

View File

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