Files
edx-platform/openedx/core/djangoapps/util/ratelimit.py
Tim McCormack a251d18281 feat!: Use more-trusted IP in rate-limiting (#241)
Previously, our rate-limiting code trusted the entire `X-Forwarded-For`
header, allowing a malicious client to spoof that header and evade
rate-limiting. This commit introduces a new module and setting
allowing us to make a more conservative choice of IPs.

- Create new `openedx.core.djangoapps.util.ip` module for producing
  the IP "external chain" for requests based on the XFF header and the
  REMOTE_ADDR.
- Include a function that gives the safest choice of IPs.
- Add new setting `CLOSEST_CLIENT_IP_FROM_HEADERS` for configuring how
  the external chain is derived (i.e. setting the trust
  boundary). Currently has a default, but we may want to make it
  mandatory in the future.
- Change `django-ratelimit` code to use the proximate IP in the external
  chain -- the one just outside the trust boundary.

Also:

- Change `XForwardedForMiddleware` to use more conservative choice for
  its `REMOTE_ADDR` override
- Other adjustments to `XForwardedForMiddleware` as needed in order to
  initialize new module and support code that needs the real
  `REMOTE_ADDR` value
- Metrics for observability into the change (and XFF composition)
- Feature switch to restore legacy mode if needed

This also gives us a path forward to removing use of the django-ipware
package, which is no longer maintained and has a handful of bugs that make it
difficult to use safely.

Internal ticket: ARCHBOM-2056
2022-04-20 13:46:58 +00:00

55 lines
1.5 KiB
Python

"""
Code to get ip from request.
"""
from uuid import uuid4
from openedx.core.djangoapps.util import ip
def real_ip(group, request): # pylint: disable=unused-argument
"""
Get a client IP suitable for use in rate-limiting.
To prevent evasion of rate-limiting, use the safest (rightmost) IP in the
external IP chain.
(Intended to be called by ``django-ratelimit``, hence the unused argument.)
"""
if ip.USE_LEGACY_IP.is_enabled():
return ip.get_legacy_ip(request)
else:
return ip.get_safest_client_ip(request)
def request_post_email(group, request) -> str: # pylint: disable=unused-argument
"""
Return the the email post param if it exists, otherwise return a
random id.
If the request doesn't have an email post body param, treat it as
a unique key. This will probably mean that it will not get rate limited.
This ratelimit key function is meant to be used with the user_authn/views/login.py::login_user
function. To rate-limit any first party auth. For 3rd party auth, there is separate rate limiting
currently in place so we don't do any rate limiting for that case here.
"""
email = request.POST.get('email')
if not email:
email = str(uuid4())
return email
def request_data_email(group, request) -> str: # pylint: disable=unused-argument
"""
Return the the email data param if it exists, otherwise return a
random id.
"""
email = request.data.get('email')
if not email:
email = str(uuid4())
return email