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
55 lines
1.5 KiB
Python
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
|