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

Add rate limiting for check_username_availability #35688

Open
wants to merge 4 commits into
base: master
Choose a base branch
from

Conversation

nospame
Copy link
Contributor

@nospame nospame commented Jan 28, 2025

Product Description

Adds rate limiting when checking username availability prior to signup. Invisible under normal circumstances.

Technical Summary

SAAS-16420

Based on rate_limit_two_factor_setup, this adds a rate limiter for the check_username_availability endpoint that is based on both IP (more lax, since public IPs can sometimes be shared) and session ID (more strict).

Specific rates here were chosen because they seemed "about right" for someone possibly typing and retyping an email address several times during signup, with the fastest per-session rate matching the frontend rate limit on the signup form (a 400ms delay). Open to other thoughts here as well.

Safety Assurance

Safety story

Tested locally to see that IP and session ID get stored and checked against as expected for rate limiting, and that rate limits do seem to apply correctly when requests are quick enough.

Automated test coverage

No automated tests here.

QA Plan

Not currently planning QA.

Rollback instructions

  • This PR can be reverted after deploy with no further considerations

Labels & Review

  • Risk label is set correctly
  • The set of people pinged as reviewers is appropriate for the level of risk of the change

@nospame nospame added the product/invisible Change has no end-user visible impact label Jan 28, 2025
@dimagimon dimagimon added the Risk: High Change affects files that have been flagged as high risk. label Jan 28, 2025
@nospame nospame marked this pull request as ready for review January 28, 2025 16:51
@@ -313,3 +321,89 @@ def send_mobile_experience_reminder(recipient, full_name, additional_email_conte
logging.warning(
"Can't send email, but the message was:\n%s" % message_plaintext)
raise


@run_only_when(not settings.ENTERPRISE_MODE and not settings.UNIT_TESTING)
Copy link
Contributor

@mjriley mjriley Jan 29, 2025

Choose a reason for hiding this comment

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

Should we enforce this for enterprise environments as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

_status_bad_request = 'bad_request'
_status_accepted = 'accepted'

def get_session_and_ip():
Copy link
Member

Choose a reason for hiding this comment

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

move these nested methods out (for readability) and privatize them (if it makes sense)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

if request and request.session:
return request.session.session_key, get_ip(request)
else:
return None
Copy link
Member

Choose a reason for hiding this comment

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

should return None, None

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Comment on lines 341 to 348
def get_session_and_ip():
request = get_request()
if request and request.session:
return request.session.session_key, get_ip(request)
else:
return None

def _check_for_exceeded_rate_limits(session, ip):
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we move these outside to not be nested functions?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

if request and request.session:
return request.session.session_key, get_ip(request)
else:
return None
Copy link
Contributor

Choose a reason for hiding this comment

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

needs to return None, None

Copy link
Contributor Author

Choose a reason for hiding this comment

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


session_id, ip_address = get_session_and_ip()

if ip_address and session_id:
Copy link
Contributor

Choose a reason for hiding this comment

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

Given that these are bad-faith actors, is it possible to continually create new session ids? Is this a concern?

Copy link
Contributor Author

@nospame nospame Jan 29, 2025

Choose a reason for hiding this comment

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

Not as far as I've found. If given a bad session ID, the request is rejected on the backend and the sessionid cookie gets removed. From django docs:

A MAC (Message Authentication Code) is used to protect the data against changes by the client, so that the session data will be invalidated when being tampered with.

Comment on lines 336 to 339
_status_session_rate_limited = 'session_rate_limited'
_status_ip_rate_limited = 'ip_rate_limited'
_status_bad_request = 'bad_request'
_status_accepted = 'accepted'
Copy link
Contributor

Choose a reason for hiding this comment

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

I think our standard for constants is all-caps. Can also move these to a const module.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Didn't move to a const module, but did move the rate limiter to its own file, as we seem to do in other apps (ota, motech, receiverwrapper)
7f9d328

This pattern is used in other apps with rate limiters
- run rate limiter in Enterprise environments
- return None, None fallback from _get_session_and_ip
- return isValid: False if check_username_availability is rate limited
@nospame nospame requested review from mjriley and biyeun January 29, 2025 23:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
product/invisible Change has no end-user visible impact Risk: High Change affects files that have been flagged as high risk.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants