WordPress login and XML-RPC endpoints at wp-login.php
and xmlrpc.php
respectively are the mostly widely abused targets for bots scanning the web for sites with weak passwords. Here is a simple Nginx configuration snippet that allows you to set the limit_req
depending on the request path and method.
First, define the limit_req_zone
at the http {}
level and conditionally enable it only for POST
requests to the desired request URIs by setting the limit request zone input to $binary_remote_addr
while keeping it undefined for all the un-mapped requests:
map "$request_method:$uri" $wp_post_limit_key {
POST:/wp-login.php $binary_remote_addr;
POST:/xmlrpc.php $binary_remote_addr;
}
limit_req_zone $wp_post_limit_key zone=wp_post_limit_zone:10m rate=5r/m;
where:
$wp_post_limit_key
is our custom variable name that is either unset by default or equal to$binary_remote_addr
when the current request should be rate-limited.wp_post_limit_zone
is our custom zone name that must be referenced inlimit_req
of eachserver {}
block.rate=5r/m
sets the allowed request rate from the same IP to 5 requests per minute. You can change it to2r/s
if you wanted to permit 2 request per second.
And then add it to the PHP location in each of your server {}
blocks:
location ~ \.php$ {
try_files $uri =404;
limit_req zone=wp_post_limit_zone;
// ...
}
You can define multiple limit_req_zone
configurations for different request mapping logic and apply them all to the same PHP location block.
Adjust the HTTP Response Code
By default Nginx responds with HTTP 503 Service Unavailable status code to all throttled requests. Use the limit_req_status
directive to change it to 429 Too Many Requests in either http {}
or server {}
block:
limit_req_status 429;
Oh! I like this use of the map with the method and URL.
Right!? I have been using it to also detect WebP support via
$http_accept
and then append any image requests with.webp
as they get generated on upload.Cool :) This made me make the same rule in Azure Front Door:
https://gist.github.com/soderlind/38ba10db6603db449c7f4a9333491c41
I am not sure if you fully understand Nginx’s ngx_http_limit_req_module module:
Nginx has NO *request* limit. Nginx is only offering a *rate* limit.
So when you write
> rate=5r/m sets the allowed request rate from the same IP to 5 requests per minute.
I would expect exactly that: Within a time window of 60 seconds, I can do 5 requests. It doesn’t matter if I will fire these 5 requests together in the first second or distribute equally within my time window.
But this is not what is happening. Instead, when you set “5r/m” you are instructing Nginx to apply a *rate* limit, which translate to 60 seconds / 5 requests = accepting a request every 12 seconds.
Please test it with a simple loop like
while true;
curl -i https://kaspars.net/blog/xmlrpc.php
sleep 1
done
The first request will be successful at say 03:35:00.
The next request at 03:35:01 won’t get accepted due to rate limit.
This will happen for the next 11 requests (due to “sleep 1” in our test script).
The 13th request at 03:35:13 will be the next request which will be accepted.
It is even more complicated: Internal, Nginx is using an interval of 100ms.
So yes, in the end, only 5 requests from the client within 60 seconds were accepted. But from from the behavior, it is not what I would expect in this context. Do you disagree?
It would be really great to tell a client, “Wait, you already posted 5 times to /wp-login.php in a minute, now you have to wait N seconds before you can try again” but this is not possible with stock Nginx.