Hardening Website Security – Part 1: HTTP Security Headers


It feels like almost every week there’s another news item about personal information being stolen because yet another company’s website got hacked.

Most of these attacks are perpetrated through social engineering, persuading somebody to hand over some detail which allows the hacker to gain additional privileges and, eventually, access to personal information. However, a lot are still carried out due to poor security or misconfigured websites.

This is the first in a series of articles which will aim to demystify some of the internet security principals you must get your head around if you hope to run a secure website in the 21st century.

Expertise Matrix - Manage the skill development process of your team.


The series will be consist of the following topics:

  1. HTTP Security Headers
  2. User Session Security
  3. Database Security
  4. Safely Handling User Input

Topics Not Covered

I have opted not to cover server infrastructure security concerns at this time due to the huge number of possible configurations (hosting packages or VPS, operating systems, dashboard systems, firewalls, etc.) While I may cover some of these specifically in future articles, there’s too much nuance in these subjects to be able to do them justice here.


Everything presented in this article is the result of years of experience, or trial and (frequent) error. Messing around with HTTP headers on a live site can (and likely will) lead to unexpected, possibly disastrous results. The information presented here is correct to the best of the author’s knowledge, but in matters of security we strictly advise the reader to make sure they carry out additional research and understand the dangers before making any changes to their own systems or web sites. Any code presented here is done so as example only, and may be incomplete or contain errors. Readers should be careful when copying and pasting code from any website, this site included. Int64 Software Ltd, its employees and its representatives accept no liability for damage caused by the misuse, either intentional or unintentional, of the information presented in its posts and articles.

Section Overview

When it receives a standard HTTP GET request, a web server will return the information that was requested along with certain header information. This is necessary information so that your browser knows how to handle the response data, things like the encoding method, and length of the data.

In this part of the series, we’ll be looking at the security related headers, what they do, and how to set them.

Configuring Web Servers

Configuring Web Servers for Security

Depending on your web server setup, the way you set headers will vary. Below are some of the more popular options and information on how to set headers.

Apache Web Server

In Apache you’ll need to use the “Header” directive in either your .htaccess, .httpd.conf or the VirtualHost section of your virtual host configuration file.

Header set [HeaderName] [HeaderValue]


In your Nginx “config” folder, open the “nginx.conf” file and add the following:

add_header [HeaderName] "[HeaderValue]";


Add a customHeaders section to the “<system.webServer>” section in your “Web.Config” file:

		<add name=”[HeaderName]” value=”[HeaderValue]” />


In PHP, headers can be defined at the very top of your PHP file (before any actual output has been sent, including blank spaces) using the “header” function.

header('[HeaderName]: [HeaderValue]', true);

Note that headers only need to be set once per request, not in every included PHP file. The “true” part of this command indicates that an existing header with this name should be replaced if encountered.


In your “global.asax” file add the following code:

protected void Application_BeginRequest(object sender, EventArgs e)
	HttpContext.Current.Response.AddHeader(“[HeaderName]”, “[HeaderValue]”);

The Headers

HTTP Headers for Security


The “X-Frame-Options” header is used to prevent “Clickjacking”, which is where an attacker attempts to make window user events such as clicking on a button do something other than the intended action. This may be done by, for example, embedding your webpage in an iframe on their own site and overlay a transparent interface over the top so that, while the user thinks they’re using your website, they’re actually feeding information to the attacker.

By setting X-Frame-Options you can tell the client browser that this page should not be allowed to load in a frame or iframe.

X-Frame-Options has three valid values:


This outright prevents any domain from rendering the page in a frame. This is the recommended setting for all pages except where a need has been specifically identified.


This will allow you to load your own content in frames, but will not allow it on other domains (including sub-domains).


This prevents the page from being loaded in frames except for the specified URI (e.g. “ALLOW-FROM https://myothersite.com”). Note that browser support for this value is limited.

Recommended Value

X-Frame-Options should always be set to “DENY” unless a need has been identified for it to be set otherwise.

Other Notes

X-Frame-Options is still widely supported, but has been deprecated in favour of the “frame-src” Content Security Policy (CSP) directive which is covered later. It is worth still including however as CSPs are only supported in Internet Explorer from version 11, X-Frame-Options support goes back to version 8.


Designed to prevent Cross-site scripting (XSS), the “X-XSS-Protection” header allows you to enable XSS filtering in the client’s browser. XSS is an attack whereby the attacker injects scripts into your website through, for example, the use of improperly secured user input fields, and those scripts then either steal data or otherwise impact on the use of the page where that script is loaded.

A common example of this can be seen in certain recent hacking incidents where scripts were injected through compromised ad networks. The end result could be that instead of submitting payment details to the site you’re on, they get sent to a hacker instead.

The X-XSS-Protection header reduces this risk by instructing browsers to filter and block XSS attacks. It has 4 possible values:

DisablesXSS filtering (not recommended)
EnablesXSS filtering. If detected, the browser removes the unsafe code and displaysthe page as normal. This is usually the default.
1; mode=block
EnablesXSS filtering. If detected, the browser blocks the page from rendering.
1; report=<reporting-uri>
EnablesXSS filtering and sanitizes the page if detected. It will then report theviolation to the specified URI.

Recommended Value

X-XSS-Protection should be set to 1, optionally enabling theblock or reporting options as your service requires.

Other Notes

As with the X-Frame-Options, this value should be setalongside a strong Content Security Policy which fully disables the use ofinline scripts. This is covered later.


This header only has one possible value (“nosniff”) and a single purpose: it will prevent the client’s browser from interpreting files as anything other than that declared by the content type header.

When a browser downloads a file, in addition to being told what the file type is by the Content-Type header, it will perform Content Sniffing (or Media Type Sniffing or MIME Sniffing) where it uses various techniques to find out what the file is and then handle it appropriately.

However, this can lead to files which the user expects to be of one type, being another completely, which can result in attacks such as cross-site scripting or malware distribution.

Recommended Value

As mentioned above, X-Content-Type-Options should be set to “nosniff” to block this behaviour.


The HTTP Strict Transport Security (HSTS) header is used to notify a browser that it should only interact with this server over a secure HTTPS connection, and never on an unencrypted HTTP protocol.

The header has two values to set:

Indicates for how long (in seconds) the browser should remember that this site must only be accessed over HTTPS.
If specified, indicates that all sub-domains of this site should also only use secure HTTP connections.

Recommended Value

This header should be defined with the maximum age set to some appropriate values such as 1 year (31536000 seconds). Optionally, the includeSubDomains property can be set if you require it.

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


Perhaps the most important, widely supported and often difficult to understand header is the Content Security Policy. This single policy, if configured correctly, can do the most good for securing your website. But if misconfigured, can break a lot of things.

The policy’s settings are mostly concerned with specifically dictating where scripts and server resources can be loaded from.

A CSP header consists of two parts: a directive and a list of sources. Directives specify the type of resource you’d like to control, and the list of sources specify where the current directive can be safely loaded from.

Note that, as the CSP can contain multiple individual policies, you can either set one header, separating the directives with a semi-colon (;), or you can define multiple Content-Security-Policy headers.

For example, if I wanted to specify that I would allow JavaScript files from our own server and a JS repository at “js.example.com”, and style sheets only from our server, I may set a header in either of these two ways (shown in PHP).

header('Content-Security-Policy: script-src \‘self\’ js.example.com; style-src \’self\’;', true);

Or like this:

header('Content-Security-Policy: script-src \‘self\’ js.example.com;', false);
header('Content-Security-Policy: style-src \’self\’;', false);

Note that the header replace parameter is changed to “false” on the latter to allow us to set more than one header with this name.


This is what all of the other fetch directives will look to if the fetch type is not explicitly defined. So if an image is requested but the “img-src” directive isn’t set, then this directive will be used instead.

child-src (deprecated)
Previously used for web workers and nested browsing contexts such as iframes. However, because of the need to occasionally have separate policies for frames and workers, this is now deprecated in favour of (the un-deprecated) “frame-src” and “worker-src”.

Restricts sources that can be used by script interfaces (<a> pings, Fetch, XMLHttpRequest such as Ajax requests, WebSocket and EventSource)

Where fonts can be fetched from (e.g. Google Fonts)

When using or <frame> or <iframe> thisspecifies where the source can come from.

Unlike frame-src, this directive specifies which sources the current page can be loaded in a frame or iframe. Setting this to “none” works similar to setting “X-Frame-Options: DENY”.

Restrict where images can be loaded from.

Specifies where application manifest files can be sourced from.

Restricts loading of media using the <audio> and <video> HTML5 elements.

Restricts the sources of elements using the <object>, <embed> and <applet> tags.

Where resources may be pre-fetched or pre-rendered from.

Valid sources for JavaScript files. This should be tied down to ‘self’ and any third-party repositories as needed. You should never allow ‘unsafe-inline’ for scripts as this would allow for inline scripts running and therefore defeats the primary purpose of a CSP, which is to help block XSS attacks.

Restrict where CSS stylesheets can be loaded from.

Specifies where WebRTC (Web Real-Time Communications) connections can come from.

Valid sources for Worker, SharedWorker and ServiceWorker scripts.

Other Notable Directives

This can be used to restrict the URLs that can be used as the target of a form submission.

Allows you to specify a URI where CSP violations are reported to.

Stops the loading of any assets over HTTP when the current page has been loaded over HTTPS.

Sources that can be added to Directives

Allows all sources except for data streams (blob:, data:, filesystem:).

Blocks all attempts to load this resource type.

Allows loading resources from the same origin (scheme, host and port).

Data scheme sources are allowed (such as Base64 encoded images).

The specified domain is allowed.

The specified domain and all subdomains are allowed.

The specified domain is allowed only over HTTPS.

Any domain as long as it is over HTTPS.

Allows for the use of inline sources, such as element styles or script blocks (shouldn’t be used unless absolutely necessary).

Allows the use of code evaluation commands such as the JavaScript “eval()” command.

Allows a random nonce value to be added to style or script tags which match this “<noncevalue>”, the styles or scripts in this tag can then be run. Note that bypasses for this have been demonstrated in the past, so it should not be considered secure.

Allows scripts or stylesheets to be used if their SHA256 hash matches this value.

Recommended Values

This one is a bit harder to recommend an exact value for as it should be catered to your website. My policy (generally when talking about anything security related) is to lock it down as tight as you can, then find out what breaks and then either fix those things, or start loosening it up until you get everything working again. That applies here, but there may be one or two extra steps.

Let’s consider “style-src”, for example. It’s easy enough to lock it down to ‘self’, meaning that only Cascading Style Sheet (.css) files on the same source can be loaded. But what if you have a few small inline style elements? The best thing to do here is to move them into a css file, but that isn’t always possible or convenient, so you may need to allow ‘unsafe-inline’.

I figured the best way to tackle this would be to walk through one of my CSPs, why I made each decision, and what negative connotations it could have.

default-src 'none’; 

The default fall-back is to deny all resources that we haven’t specifically defined below. Since I have gone through and validated all of the other sources, nothing should be reaching this anyway, and if it does then it wasn’t added by me. Be warned that you will get caught out by this when adding new content sources which is why most people set this to “self” instead.

script-src 'self' https://js.stripe.com;

I’ve packed and minified all of my own scripts, so only ‘self’ is required for those (strictly no inline scripts). In addition to that, I use Stripe as a payment platform, so have to allow scripts to be loaded from their JavaScript repository.

connect-src ‘self’ https://api.stripe.com; 

I make fairly extensive use of Ajax calls, so ‘self’ is required. Stripe also requires the use of script interfaces, so I allow them as a source.

img-src 'self' data:; 

Obviously the page in question, having been written after the 1980’s, features images, all of which are self-hosted so ‘self’ is included. I also make use of some inline SVG graphics, so I include ‘data:’.

style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; 

Stylesheets are mostly self-hosted except for Google Fonts, so both of them are allowed. For my sins I am also guilty of (rarely but occasionally) using some inline styles, so I have allowed ‘unsafe-inline’ on this directive. Note that proof of concept attacks have been shown for reading form data using CSS injection, so this is a potential weakness you will need to weigh up.

font-src 'self' https://fonts.gstatic.com;

Another pretty simple one, we want to allow fonts to be loaded locally and from Google’s Font repository.

frame-src 'self' https://*.stripe.com https://stripe.com; 

A slightly more complicated one, Stripe allows the authorisation of 3D Secure protected cards. Unfortunately, the advice for implementing this is by using an Iframe (yeah, I hate this too). So here I’ve added Stripe as the payment processor, and ‘self’ as the authorisation process returns the iframe to a URL of your choice once complete.

For those that use Stripe, you may note that their documentation states to only add “https://js.stripe.com” as a frame source, however I’ve found problems with this and am currently working with their support team at the moment to get to the bottom of it.

frame-ancestors ‘none’;

As mentioned previously, this prevents the current page from being loaded in a frame or iframe on this site or any others. Note that I loosen this to “self” on the 3D Secure return page to allow it to be displayed in a frame on my own site.

report-uri /csp-report.php;

Any CSP violations should be reported (by the client’s browser) to this URI, where it is logged for later analysis.


Finally, even though I don’t have any mixed content, I block it as a matter of course.

Extra Notes on Other Directives

Most of the other directive sources will fallback on the default-src and get blocked. The exception being the worker-src, which will fallback on child-src and then script-src in most browsers (Chrome 59 and higher will skip child-src and go directly to default-src, and Edge 17 will skip script-src).

By not defining the ‘plugin-types’ directive, this technically defaults to allowing all plugin types (<embed>, <object>, <applet>), however because we don’t define “object-src”, it falls back to default-src and blocks all plugin types at that level.

Code Examples

C++ Code

Apache Web Server

Header set X-Frame-Options “SAMEORIGIN”
Header set X-XSS-Protection “1; mode=block”
Header set X-Content-Type-Options “nosniff”
Header set Strict-Transport-Security “max-age=31536000; includeSubDomains”
Header set Content-Security-Policy “default-src 'none'; script-src 'self' https://js.stripe.com; connect-src 'self' https://api.stripe.com; img-src 'self' data:; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; frame-src 'self' https://*.stripe.com https://stripe.com; frame-ancestors 'none'; report-uri /csp_report.php; block-all-mixed-content;”


add_header X-Frame-Options “SAMEORIGIN”
add_header X-XSS-Protection “1; mode=block”
add_header X-Content-Type-Options “nosniff”
add_header Strict-Transport-Security “max-age=31536000; includeSubDomains”
add_header Content-Security-Policy “default-src 'none'; script-src 'self' https://js.stripe.com; connect-src 'self' https://api.stripe.com; img-src 'self' data:; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; frame-src 'self' https://*.stripe.com https://stripe.com; frame-ancestors 'none'; report-uri /csp_report.php; block-all-mixed-content;”


	    <add name=”X-Frame-Options” value=”SAMEORIGIN” />
	    <add name=”X-XSS-Protection” value=”1; mode=block” />
   	    <add name=”X-Content-Type-Options” value=”nosniff” />
	    <add name=”Strict-Transport-Security” value=”max-age=31536000; includeSubDomains”/>
	    <add name=”Content-Security-Policy” value=”default-src 'none'; script-src 'self' https://js.stripe.com; connect-src 'self' https://api.stripe.com; img-src 'self' data:; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; frame-src 'self' https://*.stripe.com https://stripe.com; frame-ancestors 'none'; report-uri /csp_report.php; block-all-mixed-content;” />


header('[HeaderName]: [HeaderValue]', true);

header('X-Frame-Options: SAMEORIGIN’);
header('X-XSS-Protection: 1; mode=block’);
header('X-Content-Type-Options: nosniff’);
header('Strict-Transport-Security: max-age=31536000; includeSubDomains’);
header('Content-Security-Policy: default-src \'none\'; script-src \'self\' https://js.stripe.com; connect-src \'self\' https://api.stripe.com; img-src \'self\' data:; style-src \'self\' \'unsafe-inline\' https://fonts.googleapis.com; font-src \'self\' https://fonts.gstatic.com; frame-src \'self\' https://*.stripe.com https://stripe.com; frame-ancestors \'none\'; report-uri /csp_report.php; block-all-mixed-content;’);


protected void Application_BeginRequest(object sender, EventArgs e)
    HttpContext.Current.Response.AddHeader(“X-Frame-Options”, “SAMEORIGIN”);
    HttpContext.Current.Response.AddHeader(“X-XSS-Protection”, “1; mode=block”);
    HttpContext.Current.Response.AddHeader(“X-Content-Type-Options”, “nosniff”);
    HttpContext.Current.Response.AddHeader(“Strict-Transport-Security”, “max-age=31536000; includeSubDomains”);
      “default-src 'none'; script-src 'self' https://js.stripe.com; connect-src 'self' https://api.stripe.com; img-src 'self' data:; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; frame-src 'self' https://*.stripe.com https://stripe.com; frame-ancestors 'none'; report-uri /csp_report.php; block-all-mixed-content;”);
OverLAPS is a self-hosted web interface for Microsoft's Local Administrator Password Solution (LAPS)

What’s Next

Please see our addendum to this topic on the use of HSTS Preloading, and then continue on to part 2 where we take a look at attacks that can be made against user sessions, and how to mitigate them.

Follow us on Twitter for regular updates.

Reporting Errors

If you notice any errors, or have something you think would add to the content of this article, feel free to email support@int64software.com.

Like the article? Share with your friends: