Add Nginx headers if not already set

Dec 25, 2022 Networking Security

Recently I’ve been spending quite a bit of time on website security, adding features such as Content Security Policies (CSP) to websites to mitigate the potential of website attacks such as Cross Site Scripting (XSS).

One of the companies I work for hosts a number of client websites, so I wanted to inject some sane default response headers to mitigate potential attacks. Nginx has a built-in feature called add_header which can be used to inject a response header into every request. Unfortunately add_header does not inspect the existing application headers before adding a new header, so it just appends a duplicate header if it was already set by the application. Duplicate HTTP response headers are not valid!

There is however a neat “trick” you can do which will only add a header if it does not already exist. This will not overwrite existing headers.

On every request Nginx populates a series of variables based on the existing response headers. These variables are dynamically named based on the header name, so for instance if your application returns a X-XSS-Protection: 1; mode=block header key/value, Nginx will automatically create a $upstream_http_x_xss_protection variable. The variable naming of these variables is always $upstream_http_<lowercase_headername_with_underscores>, so Referrer-Policy: same-origin would be $upstream_http_referrer_policy.

Creating header maps

We first need to create a map for each key in the Nginx configuration to a custom variable. The maps below allow us to set a custom variable to an existing value if the existing header is not set (otherwise it will set the custom variable to '' if it exists). In this example I want to inject the following headers if they haven’t already been provided by the website: X-XSS-Protection, X-Content-Type-Options & Strict-Transport-Security.

# Set default $hdr_x_xss_protection if X-XSS-Protection does not exist
map $upstream_http_x_xss_protection $hdr_x_xss_protection {
    '' "1; mode=block";
}

# Set default $hdr_x_content_type_options if X-Content-Type-Options does not exist
map $upstream_http_x_content_type_options $hdr_x_content_type_options {
    '' "nosniff";
}

# Set default $hdr_strict_transport_security if Strict-Transport-Security does not exist
map $upstream_http_strict_transport_security $hdr_strict_transport_security {
    '' "max-age=63072000";
}

Conditionally inject headers

Then we will inject the headers using the custom variables. Please note that if the custom header value is blank (empty), then Nginx will not inject the header.

# The following are only added if they do not already exist
add_header Strict-Transport-Security $hdr_strict_transport_security;
add_header X-Content-Type-Options $hdr_x_content_type_options;
add_header X-Xss-Protection $hdr_x_xss_protection;

This will allow us to return default response headers provided the website application did not already set them.

Comments