How to block POST requests in nginx
Why block POST requests in the first place?
Some websites or resources don’t need POST requests, such as a statically generated website. It looks like POST requests also take some CPU time within nginx to process them compared with static files. This becomes visible when using the $request_time variable to customize the access log.
2024-04-02T10:14:39+00:00 404 a.b.c.d “POST /xmlrpc.php HTTP/1.1” 562 “-” “Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36” TLSv1.2/ECDHE-ECDSA-AES128-GCM-SHA256 0.218 .
The request time in the nginx log is the time between the first byte from the client till the last part of the body being returned to the client. When comparing the request time (0.218) with normal requests, it becomes more obvious. They are almost all ‘0.000’, as they can be retrieved from the disk and without much overhead delivered to the client. Especially if you pre-compile files with Brotli or Gzip.
Options to filter out POST requests
Use an if-statement and $request_method
The first option is to use if and filter on the $request_method itself. This statement can be included in the server definition of your virtual host.
if ($request_method = POST ) {
return 405;
}
This small piece of logic tells nginx that we specifically want to look for POST requests and take an action. In other words, it is like a block list where you define what you don’t want. Another way is reversing this and define an allow list instead.
if ($request_method !~ ^(GET|HEAD)$ ) {
return 405;
}
If the methods are GET or HEAD, then we continue, otherwise we return the HTTP 405 status.
An import note about this 405 has to be made. The HTTP spec says that when returning a 405, you need to specify what methods you actually do accept using the Allow header.
The actual set of allowed methods is defined by the origin server at the time of each request. An origin server MUST generate an Allow header field in a 405 (Method Not Allowed) response and MAY do so in any other response. An empty Allow field value indicates that the resource allows no methods, which might occur in a 405 response if the resource has been temporarily disabled by configuration.
There are a few ways to define this Allow header, with the first being to use add_header.
add_header Allow "GET, HEAD" always;
Most likely your configuration already defines some headers and adding one more is just a small thing. At the same time it is overhead for all legitimate requests. To limit this overhead, we can only show the header for HTTP 405 responses. In your server context (virtual host configuration) you define error_page 405 and tell what it should do. Instead of pointing it to an HTML file, we link it to a location. There we define that it should insert the header and define the allowed methods.
error_page 405 @error405;
location @error405 {
# Insert Allow header to comply with HTTP standard
add_header Allow "GET, HEAD" always;
}
Using an if-statement to process the incoming requests works, but there are alternatives, such as using a map. Let’s have a look at that option as well.
Creating a map and $request_method
The second option is creating a map that contains the request methods that we want to block, while allowing the remaining ones that are not specified. Using an if statement we then can define an action based on the request method.
# Block specific types of request methods (1=block)
map $request_method $is_blocked_method {
default 0;
POST 1;
}
server {
...
# Define the error 405 handler
error_page 405 @error405;
# Block unwanted methods
if ($is_blocked_method) {
return 405;
break;
}
location / {
try_files $uri $uri/ =404;
}
location @error405 {
# Insert Allow header to comply with HTTP standard
add_header Allow "GET, HEAD" always;
}
...
}
Note: map requests can be added to the http context, not to server. So define it above the definition of the virtual host.
Using the nginx limit_except option
Nginx has the option limit_except which defines what methods are accepted without any restrictions. This is a great way to filter out requests if you only want to allow one type of requests, like GET. It means anything else will be blocked, such as POST requests. Good to know is that when you define GET it will also inherit HEAD. This way you won’t break this common method.
server {
...
location / {
# Limit everything NOT being GET (and HEAD), e.g. POST is not allowed
# To allow some systems doing a POST, define the 'allow'
limit_except GET {
# allow 1.2.3.4;
deny all;
}
try_files $uri $uri/ =404;
}
...
}
This method takes only three lines! Instead of a 405, it returns a 403. Therefore, no need for adding an Allow header.
Applying the configuration
After making the changes, test your configuration
# nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
Then reload your configuration
service nginx reload
With this action we made your configuration a bit more secure again. What is the next step to further harden your nginx configuration?