How to Rate Limit Requests to Specific PHP Files with Nginx

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 in limit_req of each server {} block.
  • rate=5r/m sets the allowed request rate from the same IP to 5 requests per minute. You can change it to 2r/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; 

4 Comments

  1. Josh Betz says:

    Oh! I like this use of the map with the method and URL.

    • Kaspars says:

      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.

  2. Igor says:

    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.

Leave a Reply