Handling the Upgrade-Insecure-Requests header in nginx

Published on

Today it is considered a de facto security standard for the web, that all HTTP requests automatically be redirected to HTTPS to prevent Man-in-the-middle attacks. Of course, this is a good standard to conform to, but there can also be benefits with providing an insecure HTTP version of your site as a fallback for users who, for one reason or another, are not able to use HTTPS. You can read more about that here.

Luckily, the web specifications have a way for the client to tell the server whether or not it wants insecure connections to be upgraded or not: The Upgrade-Insecure-Requests header. By setting this header to 1, the client can indicate to the server, that it wants any insecure HTTP requests to automatically be upgraded to HTTPS. This has also been implemented in all major browsers since 2018. This article will show you how to handle this header using nginx.

Table of contents Configuring nginx
Using Cloudflare
Testing the configuration

Configuring nginx

In its essence, the configuration is very simple: For every request, we want to check if the scheme is http and the Upgrade-Insecure-Requests header is set to 1. If both of these conditions match, we want to redirect to the same URL using HTTPS.

Sadly, you cannot have if-statements in nginx with multiple conditions, so we will have to do a workaround. What we can do is combine the current scheme with the value of the header into a string and match that against a specific value. This can be done using map in nginx:

map "$scheme+$http_upgrade_insecure_requests" $upgrade {
	default 0;
	"http+1" 1;
}

Here we combine the two values into a string with a + in the middle, and compare it against the string "http+1". If it matches, the $upgrade variable is set to 1, otherwise 0.

Now we just need to redirect based on this variable. This must be done inside the server block, while the variable must be defined in the global scope. Make sure also that the server is set to listen on both port 80 and 443.

if ($upgrade) {
	return 301 https://$host$request_uri;
}

Putting it all together, we now have a functioning nginx configuration which redirects properly based on this header:

map "$scheme+$http_upgrade_insecure_requests" $upgrade {
	default 0;
	"http+1" 1;
}

server {
	listen 80;
	listen 443 ssl;

	server_name example.com;

	location / {
		# ...
	}

	if ($upgrade) {
		return 301 https://$host$request_uri;
	}
}

Using Cloudflare

If you are using Cloudflare, you will have to make some changes to this configuration. In this case, all requests your web server receives will be either HTTP or HTTPS depending on your Cloudflare configuration, regardless of the actual user's request.

Luckily, Cloudflare sends a header along with its request, telling whether the user requested HTTP or HTTPS. This one is called CF-Visitor, and the value is actually a JSON string that could tell more information about the request, but currently it only contains a single key (the scheme). This makes it very easy to handle, as we can just continue using string matching.

map "$http_cf_visitor+$http_upgrade_insecure_requests" $upgrade {
	default 0;
	"{\"scheme\":\"http\"}+1" 1;
}

Changing the map-statement from before to the above should make the configuration work with Cloudflare. Although this solution is not completely future-proof, Cloudflare has kept it like this for long enough, that I consider it pretty safe. If they change it, HTTPS redirection will be completely disabled.

Testing the configuration

You can use curl to make sure the configuration works properly. The --head option sends a HEAD request and makes curl output the response code and headers.

curl --head http://example.com

This should give a response code of 200 and not redirect. Now you can add the header to the command:

curl --head http://example.com --header "Upgrade-Insecure-Requests: 1"

This should result in a 301 response with the Location header set to the same URL but with HTTPS. As a last check, you can test the HTTPS URL and make sure it also returns 200.

Back