Search code examples
laravelnginxhsts

nginx error_page 404 HSTS header missing


I have a Laravel site running nginx 1.15.0. The site config specifies HSTS (HTTP Strict Transport Security) headers at the server level. This works just fine for all valid URLs.

However, when requesting a resource that results in a 404, the HSTS header is not returned with the response. This is also true of other headers set by add_header in the server block.

What I'm trying to do is get the HSTS header included even in all responses, even for an error. To be honest, it's just to satisfy the security scanners flagging it as a medium-level vulnerability. It may be security theater, but I'd still like to understand what's going on here.

With one explicitly-defined exception for .json URLs, there are no other add_header directives that would be interfering with those in the server level.

Here is the content of my nginx configuration for this site. The includes before/* and after/* do not appear to be issuing any add_header directives so I'm not expanding those here.

# FORGE CONFIG (DOT NOT REMOVE!)
include forge-conf/example.com/before/*;

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name .example.com;
    root /home/forge/example.com/current/public;

    client_max_body_size 100M;

    # FORGE SSL (DO NOT REMOVE!)
    ssl_certificate /etc/nginx/ssl/example.com/302491/server.crt;
    ssl_certificate_key /etc/nginx/ssl/example.com/302491/server.key;

    ssl_protocols TLSv1.2;
    # Updated cipher suite per Mozilla recommendation for Modern compatibility
    # https://wiki.mozilla.org/Security/Server_Side_TLS#Modern_compatibility
    ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256';
    ssl_prefer_server_ciphers on;
    ssl_dhparam /etc/nginx/dhparams.pem;

    add_header X-Frame-Options "SAMEORIGIN";
    add_header X-XSS-Protection "1; mode=block" always;
    add_header X-Content-Type-Options "nosniff";
    add_header Vary "Origin";

    add_header Access-Control-Allow-Origin "*";
    add_header Access-Control-Allow-Credentials 'true';
    add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
    add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';

    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload";
    add_header Referrer-Policy "strict-origin-when-cross-origin";
    add_header Public-Key-Pins 'pin-sha256="hpkppinhash="; pin-sha256="anotherpinhash="; pin-sha256="yetanotherpinhash="; pin-sha256="anotherpinhash="; pin-sha256="lastpinhash="; max-age=86400';


    index index.html index.htm index.php;

    charset utf-8;

    # FORGE CONFIG (DOT NOT REMOVE!)
    include forge-conf/example.com/server/*;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location = /favicon.ico { access_log off; log_not_found off; }
    location = /robots.txt  { access_log off; log_not_found off; }

    access_log off; 
    error_log  /var/log/nginx/example.com-error.log error;

    error_page 404 /index.php;

    location ~ \.php$ {
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass unix:/var/run/php/php7.1-fpm.sock;
        fastcgi_index index.php;
        include fastcgi_params;
    }

    location ~ /\.ht {
        deny all;
    }

    location ~* \.json {
    add_header Cache-Control "no-store,no-cache";
        add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload";
        add_header Referrer-Policy "strict-origin-when-cross-origin";
    }
}

# FORGE CONFIG (DOT NOT REMOVE!)
include forge-conf/example.com/after/*;


Solution

  • You need to add the always parameter as stated in the documentation:

    Adds the specified field to a response header provided that the response code equals 200, 201 (1.3.10), 204, 206, 301, 302, 303, 304, 307 (1.1.16, 1.0.13), or 308 (1.13.0). The value can contain variables.

    ...

    If the always parameter is specified (1.7.5), the header field will be added regardless of the response code.

    So change your config to this:

    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;