mirror of
https://github.com/mozilla/gecko-dev.git
synced 2025-02-26 12:20:56 +00:00
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:
parent
270e7e2200
commit
b81e69ee95
@ -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 {
|
||||
|
@ -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;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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.
|
||||
|
@ -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);
|
||||
};
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user