Client IP address¶
IP address is an extremely common rate limit key, so it is important to configure correctly, especially in the equally-common case where Django is behind a load balancer or other reverse proxy.
Django-Ratelimit is not the correct place to handle reverse proxies and adjust the IP address, and patches dealing with it will not be accepted. There is too much variation in the wild to handle it safely.
This is the same reason Django dropped
SetRemoteAddrFromForwardedFor middleware in 1.1: no such “mechanism
can be made reliable enough for general-purpose use” and it “may lead
developers to assume that the value of
REMOTE_ADDR is ‘safe’.”
Mishandling client IP data creates an IP spoofing vector that allows attackers to circumvent IP ratelimiting entirely. Consider an attacker with the real IP address 220.127.116.11 that adds the following to a request:
A misconfigured web server may pass the header value along, e.g.:
X-Forwarded-For: 18.104.22.168, 22.214.171.124
Alternatively, if the web server sends a different header, like
X-Real-IP, and passes along the
X-Forwarded-For header unchanged, a mistake in ratelimit or
a misconfiguration in Django could read the spoofed header instead of
the intended one.
There are two options, configuring django-ratelimit or adding global middleware. Which makes sense depends on your setup.
Writing a small middleware class to set
REMOTE_ADDR to the actual
client IP address is generally simple:
class ReverseProxy(object): def process_request(self, request): request.META['REMOTE_ADDR'] = # [...]
# [...] depends on your environment. This middleware should be
close to the top of the list:
MIDDLEWARE_CLASSES = ( 'path.to.ReverseProxy', # ... )
@ratelimit decorator can be used with the
Alternatively, if the client IP address is in a simple header (i.e. a
X-Real-IP that only contains the client IP, unlike
X-Forwarded-For which may contain intermediate proxies) you can use
Brute force attacks¶
One of the key uses of ratelimiting is preventing brute force or dictionary attacks against login forms. These attacks generally take one of a few forms:
- One IP address trying one username with many passwords.
- Many IP addresses trying one username with many passwords.
- One IP address trying many usernames with a few common passwords.
- Many IP addresses trying many usernames with one or a few common passwords.
Unfortunately, the fourth case of many IPs trying many usernames can be difficult to distinguish from regular user behavior and requires additional signals, such as a consistent user agent or a common network prefix.
Protecting against the single IP address cases is easy:
@ratelimit(key='ip') def login_view(request): pass
Also limiting by username and password provides better protection:
@ratelimit(key='ip') @ratelimit(key='post:username') @ratelimit(key='post:password') def login_view(request): pass
Key values are never stored in a raw form, even as cache keys.
Denial of Service¶
However, limiting based on field values may open a denial of service vector against your users, preventing them from logging in.
For pages like login forms, consider implenting a soft blocking
mechanism, such as requiring a captcha, rather than a hard block with a
Network Address Translation¶
Depending on your profile of your users, you may have many users behind NAT (e.g. users in schools or in corporate networks). It is reasonable to set a higher limit on a per-IP limit than on a username or password limit.