Rate limit alias creation to prevent abuse (#2021)

* Rate limit alias creation to prevent abuse

* Limit in secs

* Calculate bucket time

* fix exception

* Tune limits
This commit is contained in:
Adrià Casajús 2024-01-30 18:29:59 +01:00 committed by GitHub
parent b8dad2d657
commit d12e776949
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 44 additions and 1 deletions

View file

@ -27,7 +27,7 @@ from sqlalchemy.orm import deferred
from sqlalchemy.sql import and_
from sqlalchemy_utils import ArrowType
from app import config
from app import config, rate_limiter
from app import s3
from app.db import Session
from app.dns_utils import get_mx_domains
@ -1563,6 +1563,15 @@ class Alias(Base, ModelMixin):
flush = kw.pop("flush", False)
new_alias = cls(**kw)
user = User.get(new_alias.user_id)
if user.is_premium():
limits = ((50, 1), (200, 7))
else:
limits = ((10, 1), (20, 7))
# limits is array of (hits,days)
for limit in limits:
key = f"alias_create_{limit[1]}d:{user.id}"
rate_limiter.check_bucket_limit(key, limit[0], limit[1] * 86400)
email = kw["email"]
# make sure email is lowercase and doesn't have any whitespace

31
app/rate_limiter.py Normal file
View file

@ -0,0 +1,31 @@
from datetime import datetime
from typing import Optional
import redis.exceptions
import werkzeug.exceptions
from limits.storage import RedisStorage
from app.log import log
lock_redis: Optional[RedisStorage] = None
def set_redis_concurrent_lock(redis: RedisStorage):
global lock_redis
lock_redis = redis
def check_bucket_limit(
lock_name: Optional[str] = None,
max_hits: int = 5,
bucket_seconds: int = 3600,
):
# Calculate current bucket time
bucket_id = int(datetime.utcnow().timestamp()) % bucket_seconds
bucket_lock_name = f"bl:{lock_name}:{bucket_id}"
try:
value = lock_redis.incr(bucket_lock_name, bucket_seconds)
if value > max_hits:
raise werkzeug.exceptions.TooManyRequests()
except redis.exceptions.RedisError:
log.e("Cannot connect to redis")

View file

@ -2,6 +2,7 @@ import flask
import limits.storage
from app.parallel_limiter import set_redis_concurrent_lock
from app.rate_limiter import set_redis_concurrent_lock as rate_limit_set_redis
from app.session import RedisSessionStore
@ -10,12 +11,14 @@ def initialize_redis_services(app: flask.Flask, redis_url: str):
storage = limits.storage.RedisStorage(redis_url)
app.session_interface = RedisSessionStore(storage.storage, storage.storage, app)
set_redis_concurrent_lock(storage)
rate_limit_set_redis(storage)
elif redis_url.startswith("redis+sentinel://"):
storage = limits.storage.RedisSentinelStorage(redis_url)
app.session_interface = RedisSessionStore(
storage.storage, storage.storage_slave, app
)
set_redis_concurrent_lock(storage)
rate_limit_set_redis(storage)
else:
raise RuntimeError(
f"Tried to set_redis_session with an invalid redis url: ${redis_url}"