2

return in server block

The Securely Deploy a Django App With Gunicorn, Nginx, & HTTPS uses the following HTTP-to-HTTPS redirect block:

# Redirect HTTP to HTTPS
server {
  server_name             .supersecure.codes;
  listen                  80;
  return                  307 https://$host$request_uri;
}

I interpret this to do a 307 redirect for every HTTP request on port 80 where the Host HTTP header matches.

return in location

The Mozilla SSL Configuration Generator uses the following block to do the same HTTP-to-HTTPS redirection (for example here):

server {
    listen 80 default_server;
    listen [::]:80 default_server;

    location / {
        return 301 https://$host$request_uri;
    }
}

I presume it is put in a location block in case someone wants to serve other things (e.g., static assets) via HTTP then it is less work to add it.

return in if block

This one is from an old server in production:

server {
    
    if ($host = some.org) {
        return 301 https://$host$request_uri;
    }

    listen 80;
    server_name some.org;
    return 404;
}

I can only guess why the if block, so I presume that the Host HTTP header is explicitly checked for the right domain, and if it doesn't match, then a 404 is returned. Otherwise, it is kicked over to the server block where TLS is set up.

Are there any merits to use this form?

edit: Found the Security issue with redirects added by Certbot's Nginx plugin post in Let's Encrypt's forum giving a rationale how a return inside an if block is more secure. Still don't understand why exactly, but it's a start. There is also the open certbot issue #9705 asking the same.

edit-2: The relevant paragraph explaining why a "bare" return 301 is harmful is posted below. (@Esa Jokinen: The reason I opted not to create an answer is because I don't understand this.)

The way Certbot creates an HTTP to HTTPS redirect in Nginx is through the following configuration directive:

return 301 https://$host$request_uri;

The potential problem with this is it relies on the value of the Host header for where to redirect the request. If a directive like this is added to the default server block for HTTP traffic in your Nginx configuration, users can modify the Host header of their request to have it redirected to an arbitrary domain. On its own, this isn’t a problem because the redirect to an arbitrary domain is only sent to user who put that domain in the Host header of their request.

The problem occurs if an attacker can trick a cache somewhere to store this redirect from Nginx and serve it to users who attempt to connect to the server through normal means with unmodified Host headers. If this could be done, an attacker could redirect traffic from users of that cache to a domain of their choosing.

Due to the prevalence of virtual hosting, a cache that would do this would have to have its own bugs or be misconfigured. The redirect added by Certbot is not exploitable on its own, but when coupled with other software with its own bugs, exploits, or misconfiguration, it can become a security problem.

toraritte
  • 200
  • 10
  • 1
    The article in your edit explains the rationale, which is in my opinion quite far fetched to become a default configuration. If you find that paragraph, you should move the edit into an answer. – Esa Jokinen Aug 09 '23 at 03:39
  • 1
    On the final example, that server block may also process requests where `server_name` does not match. The `if...return` is a trivial method for returning 404 for any other domain name that hits that server block. – Richard Smith Aug 09 '23 at 07:20
  • 1
    Relevant Server Fault thread on how NGINX processes requests: [Should there be a `server_name` directive present if only one website is served with NGINX?](https://serverfault.com/questions/1141063/should-there-be-a-server-name-directive-present-if-only-one-website-is-served/) – toraritte Aug 10 '23 at 11:34

1 Answers1

2

context matters

It really depends what your needs are

Only / first / default_server

When you only have a single server {} block for plain HTTP on TCP port 80, or you have implicitly/explicitly configured default/catch-all server in a configuration that also contains additional server blocks that will be used for virtual servers, that server will typically not only process request for sites that you actually host.

That server will also process requests for your bare IP-address(es), for DNS entries that a previous user of your IP-address hasn't cleaned up, but which still resolve to your IP-address, and/or maybe wildcard DNS entries exist.

It is unlikely that you will have valid TLS certificates for your IP-address, for DNS names that you never owned and/or no longer host, and maybe you don't have wild-card TLS certificate to match a wild-card DNS record.

When you configure an unconditional parameterised redirect in that server like:

 return 301 https://$host$request_uri;

that will redirect every request to the HTTPS version of the original request, even for sites for which you don't have a valid TLS certificate.

That can therefore result in an error for the site visitor.

What to use instead:

That is a question of opinion / style and preference.

  1. Don't use a parametrised redirect, but redirect to a specific site, for which you DO have that valid TLS certificate.

     return 301 https://www.example.com$request_uri;
    
  2. Some people suggest to always set up a default server to return an generic error for all unqualified requests. They don't redirect elsewhere from that default_server. And to explicitly create one or more additional servers to handle "real visitors" for the actual sites that are hosted on that webserver. Then they do not disclose the sites they operate to visitors that visit the bare IP-address. Different servers also allows you to set up different log files to track how many plain HTTP request your different servers still handle.

    server {
       listen 80 default_server; 
       server_name _; # This is just an invalid value which will never trigger on a real hostname.
    
        server_name_in_redirect off;
        # return a 404 error response to all unqualified requests 
        return 404;
    } 
    server {
        listen      80;
        server_name example.net www.example.net;
        # always redirect plain http to https 
        # and redirect bare domain to www 
        # if works as desired - change temporary to permanent redirect 
        return 301 https://www.example.net$request_uri;
    }    
    
  3. The if block example that you posted does more or less the same as the example above and is one method to filter and redirect to HTTPS only requests for sites for which you do have a TLS site, but in a single server block:

    server {
    
        if ($host = some.org) {
            return 301 https://$host$request_uri;
        }
    
        listen 80;
        server_name some.org;
        return 404;
    }
    

    Note: personally I wouldn't use a parameterised redirect in this construct either since nginx will always redirect to https:/some.org$request_uri; anyway.

Additional servers

server {
   #default server
}
server {
   #first additional virtual server
}
server {
   #second additional virtual server
}

When you already have a configuration that contains additional server blocks that are used for virtual servers, those will only be used when a site visitor sends the correct Host: header and those servers don't have to deal with unqualified requests.

In the additional/virtual servers the if ($host = some.org)conditional redirect would be completely redundant, since that virtual server will only process requests for that particular host anyway.

server {
   #default server
}
server {
   #first virtual server
   listen 80;
   server_name example.org;

   # Although valid syntax logically this "if" construct is completely redundant here
   # this server will only process requests for example.org anyway
   if ($host = example.org) {
       return 301 https://$host$request_uri;
   }
    
server {
   #second virtual server
   listen 80;
   server_name example.com www.example.com;

   # this redirects www.example.com to https://www.example.com
   # this redirects example.com to https://example.com 
   # that only makes sense when those are either completely different sites
   # OR they are one and the same. 

   # The parameterised redirect doesn't make sense when subsequently
   #  https://example.com redirects to https://www.example.com or vice-versa

   return 301 https://$host$request_uri;
   
   # The above  is functionally completely equivalent to 
   # enclosing the redirect in a location block as done below

   location / {
       return 301 https://$host$request_uri;
   }
        
HBruijn
  • 77,029
  • 24
  • 135
  • 201