articles/static/upgrade-insecure-requests.html

167 lines
5.7 KiB
HTML

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="author" content="Reimar" />
<meta name="viewport" content="width=device-width" />
<title>Handling the Upgrade-Insecure-Requests header in nginx</title>
<link rel="stylesheet" href="assets/style.css" />
</head>
<body>
<h1>Handling the Upgrade-Insecure-Requests header in nginx</h1>
<span class="published">Published on <time datetime="2025-08-02">August 2nd, 2025</time></span>
<p>
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
<a href="https://1mb.club/blog/https-redirects/">here</a>.
</p>
<p>
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
<a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Upgrade-Insecure-Requests">
<code>Upgrade-Insecure-Requests</code><!--
--></a>
header.
By setting this header to <code>1</code>, 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.
</p>
<fieldset class="table-of-contents">
<legend>Table of contents</legend>
<a href="#configuring">Configuring nginx</a><br>
<a href="#cloudflare">Using Cloudflare</a><br>
<a href="#testing">Testing the configuration</a>
</fieldset>
<h2 id="configuring"><a href="#configuring">Configuring nginx</a></h2>
<p>
In its essence, the configuration is very simple: For every
request, we want to check if the scheme is <code>http</code>
and the <code>Upgrade-Insecure-Requests</code> header is set
to <code>1</code>. If both of these conditions match, we
want to redirect to the same URL using HTTPS.
</p>
<p>
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 <code>map</code> in nginx:
</p>
<pre>
map "$scheme+$http_upgrade_insecure_requests" $upgrade {
default 0;
"http+1" 1;
}</pre>
<p>
Here we combine the two values into a string with a
<code>+</code> in the middle, and compare it against the
string <code>"http+1"</code>. If it matches, the
<code>$upgrade</code> variable is set to <code>1</code>,
otherwise <code>0</code>.
</p>
<p>
Now we just need to redirect based on this variable. This
must be done inside the <code>server</code> 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.
</p>
<pre>
if ($upgrade) {
return 301 https://$host$request_uri;
}</pre>
<p>
Putting it all together, we now have a functioning nginx
configuration which redirects properly based on this header:
</p>
<pre>
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;
}
}</pre>
<h2 id="cloudflare"><a href="#cloudflare">Using Cloudflare</a></h2>
<p>
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.
</p>
<p>
Luckily, Cloudflare sends a header along with its request,
telling whether the user wanted HTTP or HTTPS. This one is
called
<a href="https://developers.cloudflare.com/fundamentals/reference/http-headers/#cf-visitor">
<code>CF-Visitor</code><!--
--></a>,
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.
</p>
<pre>
map "$http_cf_visitor+$http_upgrade_insecure_requests" $upgrade {
default 0;
"{\"scheme\":\"http\"}+1" 1;
}</pre>
<p>
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.
</p>
<h2 id="testing"><a href="#testing">Testing the configuration</a></h2>
<p>
You can use curl to make sure the configuration works
properly. The <code>--head</code> option sends a
<code>HEAD</code> request and makes curl output the response
code and headers.
</p>
<pre>
curl --head http://example.com</pre>
<p>
This should give a response code of 200 and not redirect.
Now you can add the header to the command:
</p>
<pre>
curl --head http://example.com --header "Upgrade-Insecure-Requests: 1"</pre>
<p>
This should result in a 301 response with the
<code>Location</code> 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.
</p>
<p><a href="index.html">Back</a></p>
</body>
</html>