Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Unauthenticated api throttling #20993

Merged
merged 6 commits into from
Mar 19, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions posthog/api/authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
from rest_framework.exceptions import APIException
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.throttling import UserRateThrottle
from sentry_sdk import capture_exception
from social_django.views import auth
from two_factor.utils import default_device
Expand All @@ -33,15 +32,16 @@
)

from posthog.api.email_verification import EmailVerifier
from posthog.api.services.unauthenticated_rate_limiter import UserOrEmailRateThrottle
from posthog.email import is_email_available
from posthog.event_usage import report_user_logged_in, report_user_password_reset
from posthog.models import OrganizationDomain, User
from posthog.tasks.email import send_password_reset
from posthog.utils import get_instance_available_sso_providers


class UserPasswordResetThrottle(UserRateThrottle):
rate = "6/day"
class UserPasswordResetThrottle(UserOrEmailRateThrottle):
rate = "6/hour"


@csrf_protect
Expand Down Expand Up @@ -190,6 +190,7 @@ class LoginViewSet(NonCreatingViewSetMixin, viewsets.GenericViewSet):
queryset = User.objects.none()
serializer_class = LoginSerializer
permission_classes = (permissions.AllowAny,)
# NOTE: Throttling is handled by the `axes` package
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a note here as I wasn't sure why we had no throttler for logging in



class TwoFactorSerializer(serializers.Serializer):
Expand Down
22 changes: 22 additions & 0 deletions posthog/api/services/unauthenticated_rate_limiter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import hashlib
from rest_framework.throttling import SimpleRateThrottle


class UserOrEmailRateThrottle(SimpleRateThrottle):
"""
Typically throttling is on the user or the IP address.
For unauthenticated signup/login requests we want to throttle on the email address.
"""

scope = "user"

def get_cache_key(self, request, view):
if request.user and request.user.is_authenticated:
ident = request.user.pk
else:
# For unauthenticated requests, we want to throttle on something unique to the user they are trying to work with
# This could be email for example when logging in or uuid when verifying email
ident = request.data.get("email") or request.data.get("uuid") or self.get_ident(request)
ident = hashlib.sha256(ident.encode()).hexdigest()

return self.cache_format % {"scope": self.scope, "ident": ident}
17 changes: 17 additions & 0 deletions posthog/api/test/test_authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,23 @@ def test_cant_reset_more_than_six_times(self):
# Three emails should be sent, fourth should not
self.assertEqual(len(mail.outbox), 6)

def test_is_rate_limited_on_email_not_ip(self):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'm lazily assuming there's a test in here that shows the reverse (if we have the same IP but varying addresses then we don't limit (or we're slower to limit))

set_instance_setting("EMAIL_HOST", "localhost")

for email in ["[email protected]", "[email protected]"]:
for i in range(7):
with self.settings(CELERY_TASK_ALWAYS_EAGER=True, SITE_URL="https://my.posthog.net"):
response = self.client.post("/api/reset/", {"email": email})
if i < 6:
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
else:
# Fourth request should fail
self.assertEqual(response.status_code, status.HTTP_429_TOO_MANY_REQUESTS)
self.assertDictContainsSubset(
{"attr": None, "code": "throttled", "type": "throttled_error"},
response.json(),
)

# Token validation

def test_can_validate_token(self):
Expand Down
7 changes: 4 additions & 3 deletions posthog/api/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
from rest_framework.exceptions import NotFound
from rest_framework.permissions import IsAuthenticated, AllowAny
from rest_framework.response import Response
from rest_framework.throttling import UserRateThrottle

from two_factor.forms import TOTPDeviceForm
from two_factor.utils import default_device

Expand All @@ -33,6 +33,7 @@
from posthog.api.decide import hostname_in_allowed_url_list
from posthog.api.email_verification import EmailVerifier
from posthog.api.organization import OrganizationSerializer
from posthog.api.services.unauthenticated_rate_limiter import UserOrEmailRateThrottle
from posthog.api.shared import OrganizationBasicSerializer, TeamBasicSerializer
from posthog.api.utils import raise_if_user_provided_url_unsafe
from posthog.auth import PersonalAPIKeyAuthentication, SessionAuthentication, authenticate_secondarily
Expand All @@ -53,7 +54,7 @@
from posthog.constants import PERMITTED_FORUM_DOMAINS


class UserAuthenticationThrottle(UserRateThrottle):
class UserAuthenticationThrottle(UserOrEmailRateThrottle):
rate = "5/minute"

def allow_request(self, request, view):
Expand All @@ -63,7 +64,7 @@ def allow_request(self, request, view):
return super().allow_request(request, view)


class UserEmailVerificationThrottle(UserRateThrottle):
class UserEmailVerificationThrottle(UserOrEmailRateThrottle):
rate = "6/day"


Expand Down
Loading