Skip to content
This repository has been archived by the owner on Apr 26, 2024. It is now read-only.

Commit

Permalink
Add ratelimiting on login (#4821)
Browse files Browse the repository at this point in the history
Add two ratelimiters on login (per-IP address and per-userID).
  • Loading branch information
babolivier authored Mar 15, 2019
1 parent 3b7ceb2 commit 899e523
Show file tree
Hide file tree
Showing 11 changed files with 259 additions and 37 deletions.
1 change: 1 addition & 0 deletions changelog.d/4821.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add configurable rate limiting to the /login endpoint.
39 changes: 28 additions & 11 deletions docs/sample_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,34 @@ rc_messages_per_second: 0.2
#
rc_message_burst_count: 10.0

# Ratelimiting settings for registration and login.
#
# Each ratelimiting configuration is made of two parameters:
# - per_second: number of requests a client can send per second.
# - burst_count: number of requests a client can send before being throttled.
#
# Synapse currently uses the following configurations:
# - one for registration that ratelimits registration requests based on the
# client's IP address.
# - one for login that ratelimits login requests based on the client's IP
# address.
# - one for login that ratelimits login requests based on the account the
# client is attempting to log into.
#
# The defaults are as shown below.
#
#rc_registration:
# per_second: 0.17
# burst_count: 3
#
#rc_login:
# address:
# per_second: 0.17
# burst_count: 3
# account:
# per_second: 0.17
# burst_count: 3

# The federation window size in milliseconds
#
federation_rc_window_size: 1000
Expand All @@ -403,17 +431,6 @@ federation_rc_reject_limit: 50
#
federation_rc_concurrent: 3

# Number of registration requests a client can send per second.
# Defaults to 1/minute (0.17).
#
#rc_registration_requests_per_second: 0.17

# Number of registration requests a client can send before being
# throttled.
# Defaults to 3.
#
#rc_registration_request_burst_count: 3.0



# Directory where uploaded images and attachments are stored.
Expand Down
12 changes: 12 additions & 0 deletions synapse/api/ratelimiting.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@

import collections

from synapse.api.errors import LimitExceededError


class Ratelimiter(object):
"""
Expand Down Expand Up @@ -82,3 +84,13 @@ def prune_message_counts(self, time_now_s):
break
else:
del self.message_counts[key]

def ratelimit(self, key, time_now_s, rate_hz, burst_count, update=True):
allowed, time_allowed = self.can_do_action(
key, time_now_s, rate_hz, burst_count, update
)

if not allowed:
raise LimitExceededError(
retry_after_ms=int(1000 * (time_allowed - time_now_s)),
)
58 changes: 40 additions & 18 deletions synapse/config/ratelimiting.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,25 +15,30 @@
from ._base import Config


class RateLimitConfig(object):
def __init__(self, config):
self.per_second = config.get("per_second", 0.17)
self.burst_count = config.get("burst_count", 3.0)


class RatelimitConfig(Config):

def read_config(self, config):
self.rc_messages_per_second = config["rc_messages_per_second"]
self.rc_message_burst_count = config["rc_message_burst_count"]

self.rc_registration = RateLimitConfig(config.get("rc_registration", {}))

rc_login_config = config.get("rc_login", {})
self.rc_login_address = RateLimitConfig(rc_login_config.get("address", {}))
self.rc_login_account = RateLimitConfig(rc_login_config.get("account", {}))

self.federation_rc_window_size = config["federation_rc_window_size"]
self.federation_rc_sleep_limit = config["federation_rc_sleep_limit"]
self.federation_rc_sleep_delay = config["federation_rc_sleep_delay"]
self.federation_rc_reject_limit = config["federation_rc_reject_limit"]
self.federation_rc_concurrent = config["federation_rc_concurrent"]

self.rc_registration_requests_per_second = config.get(
"rc_registration_requests_per_second", 0.17,
)
self.rc_registration_request_burst_count = config.get(
"rc_registration_request_burst_count", 3,
)

def default_config(self, **kwargs):
return """\
## Ratelimiting ##
Expand All @@ -46,6 +51,34 @@ def default_config(self, **kwargs):
#
rc_message_burst_count: 10.0
# Ratelimiting settings for registration and login.
#
# Each ratelimiting configuration is made of two parameters:
# - per_second: number of requests a client can send per second.
# - burst_count: number of requests a client can send before being throttled.
#
# Synapse currently uses the following configurations:
# - one for registration that ratelimits registration requests based on the
# client's IP address.
# - one for login that ratelimits login requests based on the client's IP
# address.
# - one for login that ratelimits login requests based on the account the
# client is attempting to log into.
#
# The defaults are as shown below.
#
#rc_registration:
# per_second: 0.17
# burst_count: 3
#
#rc_login:
# address:
# per_second: 0.17
# burst_count: 3
# account:
# per_second: 0.17
# burst_count: 3
# The federation window size in milliseconds
#
federation_rc_window_size: 1000
Expand All @@ -69,15 +102,4 @@ def default_config(self, **kwargs):
# single server
#
federation_rc_concurrent: 3
# Number of registration requests a client can send per second.
# Defaults to 1/minute (0.17).
#
#rc_registration_requests_per_second: 0.17
# Number of registration requests a client can send before being
# throttled.
# Defaults to 3.
#
#rc_registration_request_burst_count: 3.0
"""
36 changes: 36 additions & 0 deletions synapse/handlers/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
StoreError,
SynapseError,
)
from synapse.api.ratelimiting import Ratelimiter
from synapse.module_api import ModuleApi
from synapse.types import UserID
from synapse.util import logcontext
Expand Down Expand Up @@ -99,6 +100,10 @@ def __init__(self, hs):
login_types.append(t)
self._supported_login_types = login_types

self._account_ratelimiter = Ratelimiter()

self._clock = self.hs.get_clock()

@defer.inlineCallbacks
def validate_user_via_ui_auth(self, requester, request_body, clientip):
"""
Expand Down Expand Up @@ -568,7 +573,12 @@ def check_user_exists(self, user_id):
Returns:
defer.Deferred: (unicode) canonical_user_id, or None if zero or
multiple matches
Raises:
LimitExceededError if the ratelimiter's login requests count for this
user is too high too proceed.
"""
self.ratelimit_login_per_account(user_id)
res = yield self._find_user_id_and_pwd_hash(user_id)
if res is not None:
defer.returnValue(res[0])
Expand Down Expand Up @@ -634,6 +644,8 @@ def validate_login(self, username, login_submission):
StoreError if there was a problem accessing the database
SynapseError if there was a problem with the request
LoginError if there was an authentication problem.
LimitExceededError if the ratelimiter's login requests count for this
user is too high too proceed.
"""

if username.startswith('@'):
Expand All @@ -643,6 +655,8 @@ def validate_login(self, username, login_submission):
username, self.hs.hostname
).to_string()

self.ratelimit_login_per_account(qualified_user_id)

login_type = login_submission.get("type")
known_login_type = False

Expand Down Expand Up @@ -735,6 +749,10 @@ def _check_local_password(self, user_id, password):
password (unicode): the provided password
Returns:
(unicode) the canonical_user_id, or None if unknown user / bad password
Raises:
LimitExceededError if the ratelimiter's login requests count for this
user is too high too proceed.
"""
lookupres = yield self._find_user_id_and_pwd_hash(user_id)
if not lookupres:
Expand Down Expand Up @@ -763,6 +781,7 @@ def validate_short_term_login_token_and_get_user_id(self, login_token):
auth_api.validate_macaroon(macaroon, "login", True, user_id)
except Exception:
raise AuthError(403, "Invalid token", errcode=Codes.FORBIDDEN)
self.ratelimit_login_per_account(user_id)
yield self.auth.check_auth_blocking(user_id)
defer.returnValue(user_id)

Expand Down Expand Up @@ -934,6 +953,23 @@ def _do_validate_hash():
else:
return defer.succeed(False)

def ratelimit_login_per_account(self, user_id):
"""Checks whether the process must be stopped because of ratelimiting.
Args:
user_id (unicode): complete @user:id
Raises:
LimitExceededError if the ratelimiter's login requests count for this
user is too high too proceed.
"""
self._account_ratelimiter.ratelimit(
user_id.lower(), time_now_s=self._clock.time(),
rate_hz=self.hs.config.rc_login_account.per_second,
burst_count=self.hs.config.rc_login_account.burst_count,
update=True,
)


@attr.s
class MacaroonGenerator(object):
Expand Down
4 changes: 2 additions & 2 deletions synapse/handlers/register.py
Original file line number Diff line number Diff line change
Expand Up @@ -629,8 +629,8 @@ def register_with_store(self, user_id, token=None, password_hash=None,

allowed, time_allowed = self.ratelimiter.can_do_action(
address, time_now_s=time_now,
rate_hz=self.hs.config.rc_registration_requests_per_second,
burst_count=self.hs.config.rc_registration_request_burst_count,
rate_hz=self.hs.config.rc_registration.per_second,
burst_count=self.hs.config.rc_registration.burst_count,
)

if not allowed:
Expand Down
10 changes: 10 additions & 0 deletions synapse/rest/client/v1/login.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from twisted.web.client import PartialDownloadError

from synapse.api.errors import Codes, LoginError, SynapseError
from synapse.api.ratelimiting import Ratelimiter
from synapse.http.server import finish_request
from synapse.http.servlet import (
RestServlet,
Expand Down Expand Up @@ -97,6 +98,7 @@ def __init__(self, hs):
self.registration_handler = hs.get_registration_handler()
self.handlers = hs.get_handlers()
self._well_known_builder = WellKnownBuilder(hs)
self._address_ratelimiter = Ratelimiter()

def on_GET(self, request):
flows = []
Expand Down Expand Up @@ -129,6 +131,13 @@ def on_OPTIONS(self, request):

@defer.inlineCallbacks
def on_POST(self, request):
self._address_ratelimiter.ratelimit(
request.getClientIP(), time_now_s=self.hs.clock.time(),
rate_hz=self.hs.config.rc_login_address.per_second,
burst_count=self.hs.config.rc_login_address.burst_count,
update=True,
)

login_submission = parse_json_object_from_request(request)
try:
if self.jwt_enabled and (login_submission["type"] ==
Expand Down Expand Up @@ -285,6 +294,7 @@ def do_jwt_login(self, login_submission):
raise LoginError(401, "Invalid JWT", errcode=Codes.UNAUTHORIZED)

user_id = UserID(user, self.hs.hostname).to_string()

auth_handler = self.auth_handler
registered_user_id = yield auth_handler.check_user_exists(user_id)
if registered_user_id:
Expand Down
4 changes: 2 additions & 2 deletions synapse/rest/client/v2_alpha/register.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,8 +210,8 @@ def on_POST(self, request):

allowed, time_allowed = self.ratelimiter.can_do_action(
client_addr, time_now_s=time_now,
rate_hz=self.hs.config.rc_registration_requests_per_second,
burst_count=self.hs.config.rc_registration_request_burst_count,
rate_hz=self.hs.config.rc_registration.per_second,
burst_count=self.hs.config.rc_registration.burst_count,
update=False,
)

Expand Down
Loading

0 comments on commit 899e523

Please sign in to comment.