Application security, Network Security, Vulnerability Management

How HTTP security headers can help harden web applications

Today’s columnist, Anusha Iyer of Corsha, offers five ways to manage the security posture of machine-driven APIs. (Credit: Stock Photo, Getty Images)

The World Wide Web was not designed with security in mind. But there are simple, powerful HTML headers you can embed in web pages to make your websites and web apps more secure.

"Setting suitable headers in your web applications and web server settings is an easy way to greatly improve the resilience of your web application against many common attacks, including cross-site scripting (XSS) and clickjacking attacks," explained Invicti's Zbigniew Banach in a 2022 blog post.

Here are five important security headers, plus a few others that have been deprecated or are only partly supported, according to the OWASP Secure Headers Project, yet might still be useful.

Why and how to use security headers

HTML headers are communications between a web client and a web server concerning the handling of a web page. We'll be dealing mainly with "response" headers, sent by web servers when responding to requests from web clients, such as a browser on a computer or smartphone.

Security-specific response headers may tell the client browser to ignore code from third-party websites; use encrypted communications; or clear its cache of the web page information after the page is closed. Others target individual web vulnerabilities. Each header has one or more possible "directives," or commands that can be invoked, which can have values as simple as "on" or "off".

In the source code of a web page, response headers at the top between the <header> tags. You can also view a page's headers in a command line by typing "curl --head" followed by the full URL of the page.

If you want a quick glance at how well your security headers are protecting your site, type the site's URL into the form field at https://securityheaders.com/ (created by security expert Scott Helme) and you'll get a letter grade ranging from A+ to F, along with explanations of why.

Headers may be written into page code manually, but more likely they will be automatically generated by server software. We'll include some instructions for embedding the most widely used security headers using Apache and Nginx software.

One note: Case doesn't matter in headers, so "Content-Security-Policy" is equivalent to "content-security-policy", "SAMEORIGIN" is the same as "sameorigin", and so on. Likewise, it shouldn't matter whether quotation marks in header values are single or double.

Five security headers you should be using

Strict-Transport-Security

Strict-Transport-Security, aka STS, enforces the HSTS policy that mandates that all connections to a webpage must use the secure, encrypted HTTPS protocol.

This prevents man-in-the-middle attacks, whereby an adversary can hijack and alter the content of a webpage during its transmission from the server to the client or masquerade as another registered user by stealing session cookies.

STS is supported by all modern desktop browsers and most mobile ones. It specifies a time period during which HSTS must be enforced, measured in seconds and denoted by the "max-age" directive in the STS header.

Another STS directive, "includeSubDomains;" commands the browser to use HTTPS with all subdomains of a website, for example not just "www.foobar.com" but also "content.foobar.com", "examples.foobar.com" and so on.

Here's what the STS header looks like on the Invicti home page:

Strict-Transport-Security: max-age=31536000; includeSubDomains; preload

You can see that the maximum age of STS policy enforcement is 31,536,000 seconds, or two years, and that "includeSubDomains" has been invoked.

The last directive, "preload", counters an inherent weakness in HSTS. The first visit to a website is often unencrypted because encryption keys have yet to be exchanged between the server and the browser, creating an opportunity for an attacker to inject malicious code or steal valuable information.

The "preload" directive indicates that https://www.invicti.com is on a precompiled list of HSTS-ready websites. Because of this, the browser can refuse to load it at all without HTTPS, greatly reducing the chances of a man-in-the-middle attack during the initial connection.

To qualify for the preload list, websites must support HTTPS on all subdomains and must have an HSTS max-age of at least one year (although Facebook's index page gets away with 180 days).

If you're using an Apache server and want to enable HSTS with a max-age of one year, include subdomains and preload the site, then add this to the configuration file in your VirtualHost:

Header always set Strict-Transport-Security "max-age=31536000; includeSubdomains; preload"

If you're using Nginx, this should be added to the server block:

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

Content-Security-Policy

The powerful Content-Security-Policy (CSP) header uses a whitelist, aka an allowlist, to specify which sources can be used to supply images, scripts, images, fonts or other media to a web page; which domains, if any, can be permitted to load the page into frames in another page; and which browser plug-ins can be used. (A full list of possible CSP directives is here.) CSP is supported by all modern desktop browsers and most mobile ones.

If an item is not permitted by the CSP whitelist, it won't appear in the browser. This can result in web applications not working or important content not loading.

It may be best to initially run some CSP directives in "report mode," using the related header "content-security-policy-report-only" and specifying a web address to which logs can be sent. Once you've examined the logs and can figure out how to let desired elements load with CSP, you can slowly activate its features.

"In report-only mode, the browser will monitor the policy and report violations but without actually enforcing the restrictions," explained Invicti's Banach in a 2020 blog post. "Use the report-uri directive to tell the browser where it should post violation reports in JSON format."

CSP headers can get complicated and be hundreds of characters long. The most basic directive is to simply restrict all content sources to the same domain and block everything else, e.g.:

Content-Security-Policy: default-src "self"

But most enterprise websites need content from other sources. Here's the Content-Security-Policy header for the SC Magazine homepage:

content-security-policy: default-src data: https: 'unsafe-eval' 'unsafe-inline' 'unsafe-hashes'; report-uri /_csp; report-to default

content-security-policy-report-only: default-src data: https: 'unsafe-eval' 'unsafe-inline' 'unsafe-hashes'; img-src data: *; script-src 'unsafe-inline' 'unsafe-hashes' *; style-src 'unsafe-inline' 'unsafe-hashes' *; connect-src *; child-src *; font-src *; report-uri /_csp; report-to default

As you can see by the inclusion of "content-security-policy-report-only", SC Magazine is still trying out CSP to make sure it doesn't break things. The site is permitting almost any third-party content to get through, as indicated by including "default-src" with no value.

Likewise, "unsafe-eval" "unsafe-inline" and "unsafe-hashes" are permitting inline code, evaluation functions and hashed data that would normally be blocked by CSP. Other sites break down source restrictions by type using directives such as "script-src", "style-src", "font-src" and others.

To set up Content-Security-Policy on Apache, using the most basic directives in this example, add this to the configuration file in your VirtualHost:

Header always set Content-Security-Policy "default-src 'self';"

In Nginx, add this to your server block:

add_header Content-Security-Policy "default-src 'self';" always;

X-Content-Type-Options

Some security headers defeat specific web-based attacks. First up is X-Content-Type-Options, which prevents attackers from abusing MIME sniffing. It is supported by all modern desktop browsers and most mobile ones.

Websites are supposed to indicate what kind of content is on a page, such as audio, video, images, code or text, by using the Multipurpose Internet Mail Extensions, or MIME, standard. But sometimes one type of content is misidentified as another type, or a content type isn't listed at all.

In such cases, the browser will "sniff" the object in question and try to parse it as HTML. As a result, JavaScript embedded in text or images may be executed, permitting unauthorized code to run in the browser.

The X-Content-Type-Options header blocks the MIME sniffing mechanism. There is only one possible directive:

X-Content-Type-Options: nosniff

In Apache, set up X-Content-Type-Options by adding this to the VirtualHost configuration file:

Header always set X-Content-Type-Options "nosniff"

In Nginx, add this to the server block:

add_header X-Content-Type-Options "nosniff" always;

X-Frame-Options

Another common attack abuses iframes, sections of a web page in which other web pages can be displayed. Such attacks can trick web users into clicking things they shouldn't.

Sites that use the X-Frame-Options (XFO) header can use the directive "sameorigin" to prevent browsers from displaying them in other sites' pages or use the directive "deny" to prevent them from being included in any iframes anywhere.

A third directive, "allow-from", whitelists other domains to let them display the site in an iframe. Few browsers other than Internet Explorer 8 through 11 support this directive.

The XFO security header is supported in all desktop browsers and almost all mobile ones, but you won't see it on all web pages because the "frame-ancestors" directive in Content-Security-Policy can be used to achieve the same result.

Here's how the XFO header looks on the Facebook and Invicti home pages:

X-Frame-Options: DENY

And here's how it looks on the Google home page:

X-Frame-Options: SAMEORIGIN

To set up X-Frame-Options in Apache to allow only iframes on the same domain, add this to your VirtualHost config file:

Header always set X-Frame-Options "SAMEORIGIN"

In Nginx, add this to the server block:

add_header X-Frame-Options "SAMEORIGIN" always;

Cache-Control

The last security header you should be using is Cache-Control, which specifies how long a web page's information should be held in a browser's cache. You don't want a browser to retain financial information from a banking website, or personal information from a dating site.

Permitted directives include "no-store," which forbids caching of any kind; "no-cache" and "must-revalidate", which mean cached data must be validated by the origin server before being accessed; and "max-age" followed by a value in seconds, after which the cached data must be refreshed. A full list of possible Cache-Control directives is here.

Here's an example from https://www.cyberriskalliance.com, the website of SC Magazine's parent company:

Cache-Control: max-age=600, must-revalidate

This means that cached data must be refreshed after 10 minutes and must always be checked.

Some websites don't want to you cache anything, such as the Cisco and Microsoft home pages:

Cache-Control: max-age=0, no-cache, no-store

Cache-Control is supported by all modern desktop browsers and most mobile ones, but it does require the client to support HTTP version 1.1. Just in case, many websites also toss in the security header Pragma to interact with older browsers.

The Facebook, Microsoft and LinkedIn index pages all use Pragma thus:

Pragma: no-cache

To set up Cache-Control in Apache for all file types with a max-age of 10 minutes and a "must-revalidate" directive, add this to the VirtualHost configuration file:

Header always set Cache-Control "max-age=600, must-revalidate"

In Nginx, add this to the config file:

add_header Cache-Control "max-age=600, must-revalidate" always;

You can also specify which file types this applies to. For common web file types, use this in Apache instead:

<filesMatch ".(ico|pdf|flv|jpg|jpeg|png|gif|js|css|swf)$">

    Header always set Cache-Control "max-age=600, must-revalidate"

</filesMatch>

And in Nginx:

location ~* \.(ico|pdf|flv|jpg|jpeg|png|gif|js|css|swf)$ {

    add_header Cache-Control "max-age=600, must-revalidate" always;

}

Deprecated and draft headers that might be useful

Here are two deprecated, i.e., retired, security headers that are still widely used, plus one that's still in draft form but looks promising.

X-XSS-Protection

X-XSS-Protection guards against cross-site-scripting (XSS) attacks, whereby malicious code from another site can infect a website's code. It activates the XSS filter that was built into many browsers to block malicious code, but which also led to some problems.

"XSS filters have been abused in the past in order to block the rendering of parts of an HTML page [and] also used by attackers to disable important HTML and JavaScript code," said an Invicti white paper from 2018.

The X-XSS-Protection header never worked in Firefox, is no longer supported on most other browsers except Safari and Internet Explorer and has been replaced by Content-Security-Policy directives such as "object-src" and "reflected-xss".

But a lot of websites still include X-XSS-Protection, including CNN, Cisco and Invicti, all of which use the most common setting:

x-xss-protection: 1; mode=block

It can't hurt to use X-XSS-Protection. Setting it up in Apache involves adding this to the VirtualHost config file:

Header always set X-XSS-Protection "1; mode=block"

And here's what you add to the Nginx server block:

add_header X-XSS-Protection "1; mode=block" always;

Expect-CT

Expect-CT tells Chromium-based browsers like Chrome, Edge or Opera to check a website's security certificate for compliance with the Certificate Transparency policy to detect potential certificate misuse.

Firefox, Safari and Internet Explorer never supported Expect-CT, and it was deprecated in October 2022 with the release of Chromium 107, which enforces Certificate Transparency by default.

But like X-XSS-Protection, you'll still find the Expect-CT header on many websites. This is what it looks like on the LinkedIn home page:

Expect-CT: max-age=86400, report-uri="https://www.linkedin.com/platform-telemetry/ct"

This indicates the Certificate Transparency policy should be enforced for 24 hours and that log data should be reported to the URL indicated above.

To set up Expect-CT on Apache with 24-hour enforcement and a report URL of "https://www.foobar.com/ct-report", use this:

Header always set Expect-CT "max-age=86400, enforce, report-uri="https://www.foobar.com/ct-report"

In Nginx, that would be:

add_header Expect-CT "max-age=86400, enforce, report-uri="https://www.foobar.com/ct-report" always;

Permissions-Policy

Finally, there's Permissions-Policy, which replaces the older, deprecated Feature-Policy and is still being developed. It lets the server dictate which PC or smartphone features cannot be used with a web page, such as the camera, accelerometer, microphone, GPS and so on.

Permissions-Policy is still in draft mode and supported only by Chrome and Edge on the desktop and Chrome for Android, Opera Mobile and Baidu on mobile devices. Here's a GitHub page to keep track of which Permissions-Policy directives are supported by which versions of Chrome and their corresponding versions of Edge.

We anticipate that Permissions-Policy may someday be widely adopted by more browsers, especially mobile ones, as it's useful for supporting privacy and security. There's no need to activate it just yet, but keep an eye on it.

Paul Wagenseil

Paul Wagenseil is custom content strategist for CyberRisk Alliance, leading creation of content developed from CRA research and aligned to the most critical topics of interest for the cybersecurity community. He previously held editor roles focused on the security market at Tom’s Guide, Laptop Magazine, TechNewsDaily.com and SecurityNewsDaily.com.

Get daily email updates

SC Media's daily must-read of the most current and pressing daily news

By clicking the Subscribe button below, you agree to SC Media Terms and Conditions and Privacy Policy.