7 minute read Security

Despite years of advancements in web security, many applications still lack one of the simplest, most effective defense mechanisms: HTTP Security Headers. These small, often-overlooked settings can prevent a wide range of attacks, from clickjacking to XSS to data leaks, yet they’re missing from a surprising number of production systems.

In this post, we’ll explore what HTTP security headers are, why they matter, and how to implement them properly to protect your users and systems.

Why Security Headers Are Often Forgotten

HTTP security headers don’t require any changes to your application logic or user interface. They’re typically set at the web server, load balancer, or reverse proxy level, which often means they fall through the cracks between frontend and backend teams, or get left out entirely in “just get it working” deployments.

Yet missing them is like leaving your front door unlocked. They’re easy to add, and critical for minimising your attack surface.

The Essential HTTP Security Headers

Let’s walk through the key headers, what they do, and how to implement each one securely.

Content-Security-Policy (CSP)

Allows website administrators to control the sources of content (scripts, styles, images, fonts, etc.) your browser will load for a given page.

By restricting the sources from which browsers can load scripts, styles, images, and other resources, CSP helps prevent XSS (Cross-Site Scripting), data injection, and other code-injection attacks. This means that even if an attacker manages to inject malicious code into your site, the browser will block its execution unless it comes from an explicitly allowed source.

Example:

Content-Security-Policy: default-src 'self'; script-src 'self' https://trusted.cdn.com

Best Practices:

  • Start with ‘self’ and explicitly allow only trusted sources.
  • Avoid ‘unsafe-inline’ and ‘unsafe-eval’ unless absolutely necessary. If a CSP contains a default-src or a script-src directive, then JavaScript functions which evaluate their arguments as JavaScript and inline JavaScript or styles are disabled. The unsafe-eval and unsafe-inline keywords can be used to undo these protections, allowing dynamic evaluation of JavaScript.
  • Use a report-only mode first to detect violations before enforcing. The HTTP Content-Security-Policy-Report-Only response header helps to monitor Content Security Policy (CSP) violations and their effects without enforcing the security policies. This header allows you to test or repair violations before a specific Content-Security-Policy is applied and enforced.

Strict-Transport-Security (HSTS)

Tells browsers that the site must only be accessed via HTTPS, automatically upgrading all subsequent requests to use HTTPS.

Imagine connecting to a free public Wi-Fi network and logging into your online banking account. Unbeknownst to you, the Wi-Fi hotspot is actually controlled by an attacker, who intercepts your unsecured HTTP requests and silently redirects you to a fake version of your bank’s website. As a result, your sensitive information is at risk of being stolen.

Strict Transport Security addresses this issue by ensuring that, once you’ve visited your bank’s website over HTTPS and the site has enabled Strict Transport Security, your browser will remember to always use HTTPS for future visits. This prevents attackers from intercepting your connection through man-in-the-middle attacks.

Example:

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

Best Practices:

  • Use a long max-age e.g. 2 years. This is the time, in seconds, that the browser should remember that a site is only to be accessed using HTTPS. When the expiration time specified by the Strict-Transport-Security header elapses, the next attempt to load the site via HTTP will proceed as normal instead of automatically using HTTPS. Whenever the Strict-Transport-Security header is delivered to the browser, it will update the expiration time for that site, so sites can refresh this information and prevent the timeout from expiring. Should it be necessary to disable Strict Transport Security, setting the max-age to 0 (over an HTTPS connection) will immediately expire the Strict-Transport-Security header, allowing access via HTTP.
  • Include subdomains. If this optional parameter is specified, this rule applies to all of the site’s subdomains as well.
  • Consider submitting your domain to the HSTS preload list. Visit hstspreload.org

Note: Using the Strict-Transport-Security header is more secure than just configuring a HTTP to HTTPS (301) redirect on your server, as the initial HTTP connection is still vulnerable to a man-in-the-middle attack.

X-Content-Type-Options

Prevents browsers from MIME-sniffing content and interpreting it as a different type than declared.

The HTTP X-Content-Type-Options response header indicates that the MIME types advertised in the Content-Type headers should be respected and not changed. The header allows you to avoid MIME type sniffing by specifying that the MIME types are deliberately configured.

By enforcing the declared content type, this header stops potential content-type confusion attacks, where a browser might otherwise attempt to interpret files as a different type than intended. For example, if a script is served with an incorrect MIME type, some browsers might still try to execute it as JavaScript, opening the door to cross-site scripting (XSS) or other injection attacks. Setting X-Content-Type-Options: nosniff ensures that browsers strictly follow the specified content type, reducing the risk of malicious files being misinterpreted and executed.

Example:

X-Content-Type-Options: nosniff

Best Practices:

  • Always set to nosniff. This blocks a request if the request destination is of type style and the MIME type is not text/css, or of type script and the MIME type is not a JavaScript MIME type.
  • Especially useful for scripts and stylesheets served dynamically.

X-Frame-Options

Prevents your site from being embedded in an <iframe> on another domain.

Clickjacking is an attack where a malicious site embeds your site in a hidden or disguised iframe, tricking users into clicking on elements without their knowledge. By setting the X-Frame-Options header, you can prevent your site from being loaded inside iframes on other domains, effectively mitigating clickjacking attacks and ensuring that your content is only displayed as intended.

Example:

X-Frame-Options: DENY

Options:

  • DENY: No framing allowed.
  • SAMEORIGIN: Only allow your own site to frame it.
  • ALLOW-FROM uri (deprecated and not supported in all browsers).

Referrer-Policy

Controls how much information is sent in the Referer header when navigating between pages or sites.

Reduces the risk of leaking sensitive URL data by controlling what information is included in the Referer header when users navigate away from your site. Without a strict referrer policy, browsers may send the full URL—including query parameters or fragments—to external sites, potentially exposing sensitive data such as authentication tokens, user IDs, or internal paths. By configuring the Referrer-Policy header appropriately, you can limit the amount of information shared, ensuring that only the necessary details are sent and protecting your users’ privacy.

Example:

Referrer-Policy: no-referrer-when-downgrade

Best Practices:

  • strict-origin-when-cross-origin is a good secure default.
  • Avoid leaking full URLs across origins unless needed.

Permissions-Policy (formerly Feature-Policy)

Controls access to powerful browser APIs (e.g., geolocation, camera, microphone, fullscreen).

The Permissions-Policy header (formerly known as Feature-Policy) allows you to specify which origins are permitted to use powerful browser features such as geolocation, camera, microphone, fullscreen, and more. By explicitly restricting access to these sensitive APIs, you can prevent unauthorised or potentially malicious third-party content from leveraging them on your site. This not only reduces your attack surface but also helps protect user privacy by ensuring that only trusted sources can request access to features that may expose sensitive information or capabilities. Implementing a strict Permissions-Policy is a proactive step toward minimising risk and maintaining tighter control over your application’s behavior in the browser.

Example:

Permissions-Policy: geolocation=(), camera=()

Best Practices:

  • Deny access to features your site doesn’t need.
  • Set a tight policy by default and open access only when necessary.

Cross-Origin-Embedder-Policy / Cross-Origin-Opener-Policy / Cross-Origin-Resource-Policy

Strengthen protections against cross-origin leaks and enhance browser isolation.

These headers are more advanced but have become increasingly important for modern web applications, especially those leveraging features like SharedArrayBuffers, web workers, or embedding third-party content. By setting Cross-Origin-Embedder-Policy (COEP), Cross-Origin-Opener-Policy (COOP), and Cross-Origin-Resource-Policy (CORP), you can enforce strict isolation between your site and other origins. This helps prevent cross-origin data leaks, enables powerful browser features such as high-performance JavaScript and WebAssembly, and protects against attacks that exploit shared resources or browser process isolation. Implementing these headers is essential for applications that require strong security boundaries or need to use advanced browser capabilities safely.

Example:

Cross-Origin-Embedder-Policy: require-corp
Cross-Origin-Opener-Policy: same-origin

These are especially relevant for apps using WebAssembly or high-performance JS.

How to Implement Them

You can configure headers in various places depending on your stack:

Nginx:

add_header X-Content-Type-Options nosniff;
add_header X-Frame-Options DENY;
add_header Referrer-Policy strict-origin-when-cross-origin;
add_header Content-Security-Policy "default-src 'self';";

Apache:

Header always set X-Content-Type-Options "nosniff"
Header always set X-Frame-Options "DENY"
Header always set Referrer-Policy "strict-origin-when-cross-origin"

Express.js (Node.js):

const helmet = require('helmet');
app.use(helmet());

helmet sets a lot of these headers with good defaults out of the box.

Testing Your Headers

Use tools like:

  • SecurityHeaders.com
  • Mozilla Observatory
  • CSP Evaluator

Scan your app and compare the results to best practices.

Leave a comment