Skip to main content
Report

Why Security Headers Matter: Protecting Your Users With HTTP Response Headers

A
admin
5 min read

External Resource

Table of Contents

Introduction

What Are HTTP Security Headers?

HSTS: Enforcing Encrypted Connections

CSP: Controlling What Your Browser Executes

X-Frame-Options: Preventing Clickjacking

X-Content-Type-Options: Stopping MIME Sniffing

Referrer-Policy and Permissions-Policy

Real-World Breach Examples

How to Implement Security Headers

How ScanSentinel Checks Your Headers

Actionable Recommendations

Introduction

When your browser requests a web page, the server responds with more than just HTML. Tucked into the response are HTTP headers — metadata the browser uses to decide how to handle the content that follows. Among these are a set of security headers that act as a policy layer, telling the browser which security features to enable and what restrictions to enforce.

Implemented correctly, these headers protect your users from some of the most common web attacks: clickjacking, cross-site scripting (XSS), MIME sniffing, and protocol downgrade attacks. They require no client-side installation, no user interaction, and typically less than a dozen lines of server configuration.

Yet most websites deploy without them. Our scans at ScanSentinel consistently find that the majority of domains are missing four or more of the seven key security headers we check for. Content Security Policy — arguably the most powerful header in the set — is absent from the overwhelming majority of smaller websites.

This article explains what each security header does, what it protects against, real-world examples of what happens when they're missing, and how to implement them on your own site.

What Are HTTP Security Headers?

HTTP security headers are directives sent from the server to the browser in the HTTP response. They don't block attacks at the server level (a Web Application Firewall does that). Instead, they instruct the browser to enforce security policies on the client side. This browser-enforced model is important because many attacks — particularly XSS and clickjacking — happen entirely within the user's browser, not on your server.

Think of security headers as a set of rules you give to the browser:

"Only connect to this site over HTTPS, and only over HTTPS."

"Only execute scripts from these specific sources. If you see a script from anywhere else, block it."

"Don't let this page be embedded in a frame on another website."

"Don't guess what type of file this is — trust the Content-Type header I sent you."

"Don't send the full URL of the previous page when users click external links."

Each header addresses a specific class of vulnerability. Together, they form a defence-in-depth layer that makes exploiting your website substantially harder — even if an attacker finds a way to inject malicious content.

The seven headers we track at ScanSentinel are:

Strict-Transport-Security (HSTS): 20 points

Content-Security-Policy (CSP): 20 points

X-Content-Type-Options: 15 points

X-Frame-Options: 15 points

X-XSS-Protection: 10 points

Referrer-Policy: 10 points

Permissions-Policy: 10 points

HSTS: Enforcing Encrypted Connections

What It Does

HTTP Strict Transport Security (HSTS) tells the browser: "Always connect to this site using HTTPS. Never attempt an unencrypted HTTP connection. If the certificate doesn't validate, don't proceed — and don't let the user click through a warning."

The header looks like this:

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

The max-age directive (in seconds) tells the browser how long to remember this policy. One year (31,536,000 seconds) is the recommended minimum and is required for HSTS preload list submission. The includeSubDomains directive extends the policy to all subdomains. The preload directive indicates that you'd like to be included in browser HSTS preload lists — hard-coded lists of HTTPS-only domains shipped with every browser installation.

What It Protects Against

Without HSTS, a user who types http://example.com into their address bar (or clicks a legacy HTTP link) will attempt an unencrypted connection first. Even if your server redirects HTTP to HTTPS, that initial unencrypted request is a window of vulnerability. An attacker on the same network — public Wi-Fi is the classic scenario — can intercept that first request and serve a fake version of your site, a technique called SSL stripping.

HSTS eliminates this by telling the browser to internally upgrade any HTTP request to HTTPS, skipping the initial unencrypted connection entirely. After the first visit, the user is protected for the entire max-age duration.

Configuration Warning

A short max-age (less than one year) reduces the protection window. ScanSentinel flags HSTS configurations with a max-age under 31,536,000 seconds as a warning. Similarly, omitting includeSubDomains means subdomains remain unprotected against downgrade attacks.

CSP: Controlling What Your Browser Executes

What It Does

Content Security Policy (CSP) is a whitelist mechanism. You declare which sources the browser should trust for scripts, stylesheets, images, fonts, media, connections, and other resource types. The browser enforces these restrictions: if a script tries to load from a source not in your whitelist, it's blocked. If an inline script is present but you haven't explicitly allowed unsafe-inline, it's blocked.

A basic CSP might look like:

Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com; style-src 'self' 'unsafe-inline'; img-src 'self' data:; frame-ancestors 'none'; base-uri 'self';

This policy says: by default, only load resources from the same origin; allow scripts from our own domain and cdn.example.com; allow styles from our own domain and inline styles; allow images from our own domain and data URIs; don't allow this page to be embedded in any frame (similar to X-Frame-Options); and only allow <base> tags pointing to our own domain.

What It Protects Against

CSP is primarily an anti-XSS mechanism. If an attacker manages to inject a <script> tag into your page — through a stored XSS vulnerability in a comment system, a reflected XSS in a search parameter, or a compromised third-party library — CSP prevents the browser from executing it. The script might be in your page's HTML, but if its source isn't in your CSP whitelist, it won't run.

CSP also protects against other injection vectors. Clickjacking (via frame-ancestors), CSS injection (via style-src), and data exfiltration (via connect-src) can all be addressed with proper CSP directives.

Configuration Warnings

CSP is powerful but easy to configure poorly. ScanSentinel flags two common anti-patterns:

unsafe-inline — Allows inline <script> tags and inline event handlers (onclick, onerror, etc.), which is precisely what XSS payloads rely on. If you're using unsafe-inline, CSP is providing almost no protection against XSS.

unsafe-eval — Allows eval(), setTimeout(string), setInterval(string), and Function() constructor. These are dangerous because they execute arbitrary strings as code. Several popular frameworks require unsafe-eval in development but can work without it in production with the right build configuration.

A CSP that includes both unsafe-inline and unsafe-eval is largely cosmetic.

X-Frame-Options: Preventing Clickjacking

What It Does

X-Frame-Options controls whether your page can be embedded inside a <frame>, <iframe>, or <object> on another website.

X-Frame-Options: DENY

The possible values are:

DENY — Never allow this page to be displayed in a frame, regardless of the parent site.

SAMEORIGIN — Allow framing only by pages from the same domain. Useful for sites that frame their own content (e.g., an admin panel that embeds reports).

ALLOW-FROM uri — Allow framing only by a specific origin (obsolete and not supported by most modern browsers).

What It Protects Against

Clickjacking is an attack where a malicious website loads your page in a transparent iframe layered on top of something the user intends to click. The user thinks they're clicking a button that says "Win a Free iPad," but they're actually clicking your "Delete Account" button or authorising a financial transaction.

This technique has been used against major platforms. In 2018, researchers demonstrated clickjacking attacks against Google's account settings page. Facebook has fought clickjacking campaigns that tricked users into "liking" pages they never intended to engage with. In each case, the fix was the same: deny framing with X-Frame-Options.

Modern Alternative

CSP's frame-ancestors directive (frame-ancestors 'none' or frame-ancestors 'self') covers the same ground and is supported by all modern browsers. If you have both, the browser will respect frame-ancestors and ignore X-Frame-Options. ScanSentinel checks for both and recommends using frame-ancestors for newer applications while keeping X-Frame-Options as a fallback for older browsers.

X-Content-Type-Options: Stopping MIME Sniffing

What It Does

This header tells the browser: "Trust the Content-Type header I sent you. Don't try to guess the file type based on its contents."

X-Content-Type-Options: nosniff

The only valid value is nosniff. There is no reason to ever configure it differently.

What It Protects Against

MIME sniffing is when a browser ignores the declared Content-Type header and inspects the first few bytes of a file to determine what it is. This was originally designed to handle misconfigured servers that served content with the wrong MIME type. But it creates a security problem: an attacker who can upload a file with a .jpg extension containing JavaScript code can trigger MIME sniffing, and the browser may execute the file as JavaScript despite the server declaring it as image/jpeg.

This technique was used in real attacks against user-generated content platforms. Attackers uploaded image files containing embedded scripts. Browsers that performed MIME sniffing would execute the scripts in the context of the target domain, bypassing CSP and same-origin protections.

X-Content-Type-Options: nosniff prevents this entirely. The browser will only execute or render a resource if the server's declared Content-Type matches the resource type — no guessing allowed.

Referrer-Policy and Permissions-Policy

Referrer-Policy

When a user clicks a link from your site to an external site, the browser sends a Referer (sic) header containing the URL of the page the user was on. Depending on your application, that URL might contain sensitive information: session tokens, search queries, internal paths.

Referrer-Policy: strict-origin-when-cross-origin

This tells the browser: when navigating from one page on our site to another, send the full URL. When navigating to a different origin, send only the origin (scheme + host + port), and only if the protocol security level stays the same (HTTPS to HTTPS). If navigating from HTTPS to HTTP, send nothing.

Without this header, browsers typically send the full URL on cross-origin navigation. This leaks internal URL structure — and potentially session data — to third-party analytics, advertisers, and any site the user visits after yours.

Permissions-Policy

Permissions-Policy (formerly Feature-Policy) controls which browser APIs and features your site can use, and whether it can delegate those permissions to iframes it embeds.

Permissions-Policy: camera=(), microphone=(), geolocation=(self), payment=(self "https://checkout.example.com")

This header says: never allow access to the camera or microphone; allow geolocation only for our own origin; allow the Payment Request API for our origin and the checkout provider. All other features default to denied.

The practical benefit is defence in depth. If an attacker manages to inject JavaScript that calls navigator.geolocation.getCurrentPosition(), a restrictive Permissions-Policy prevents it from working even if the user has previously granted location permission to your site.

Real-World Breach Examples

British Airways (2018)

Attackers injected a malicious script into BA's payment page that captured credit card details as customers typed them. The script was loaded from a third-party domain that BA's Content Security Policy trusted — but the attack vector (Magecart-style card skimming) is precisely what a restrictive CSP with well-scoped script-src can mitigate. The ICO fined BA £20 million, citing insufficient security measures. Headers alone wouldn't have prevented this specific attack, but a strict CSP combined with Subresource Integrity (SRI) could have made the injection significantly harder.

Twitter Clickjacking (2009)

A worm spread through Twitter via clickjacking. Users visiting a malicious page would unknowingly click a hidden "Post Tweet" button, causing their account to tweet the worm's URL. The fix was X-Frame-Options — Twitter deployed it, and the propagation stopped. This is the canonical example of a header-level fix preventing a real, in-the-wild attack.

MIME Sniffing on User-Generated Content Sites (Various)

Throughout the 2010s, multiple social media and file-sharing platforms suffered attacks where users uploaded script-disguised-as-image files. Because the platforms didn't set X-Content-Type-Options: nosniff, browsers would MIME-sniff the files, treat them as executable scripts, and run them in the context of the origin domain. Each instance was fixed by adding a single header.

How to Implement Security Headers

Nginx

add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; frame-ancestors 'none'; base-uri 'self';" always;
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;

Apache

Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
Header always set Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; frame-ancestors 'none'; base-uri 'self';"
Header always set X-Frame-Options "DENY"
Header always set X-Content-Type-Options "nosniff"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Permissions-Policy "camera=(), microphone=(), geolocation=()"

Application-Level (Node.js / Express)

app.use((req, res, next) => {
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');
res.setHeader('Content-Security-Policy', "default-src 'self'; script-src 'self'; frame-ancestors 'none'");
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
res.setHeader('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
next();
});

Important: Test Before You Ship

CSP in particular can break functionality if your directives are too restrictive. Start with a Content-Security-Policy-Report-Only header, which reports violations without blocking them. Monitor the reports, adjust your policy, and switch to the enforcing Content-Security-Policy header once you're confident nothing legitimate breaks.

How ScanSentinel Checks Your Headers

Every ScanSentinel scan includes a comprehensive header audit. Here's exactly what we check:

Presence of all seven headers — We check whether HSTS, CSP, X-Frame-Options, X-Content-Type-Options, X-XSS-Protection, Referrer-Policy, and Permissions-Policy are present in the HTTP response.

HSTS configuration quality — We verify that the max-age is set to at least one year (31,536,000 seconds). Shorter durations receive a warning.

CSP dangerous directives — We flag unsafe-inline and unsafe-eval as warnings. If both are present, your CSP is largely ineffective.

X-XSS-Protection disabled — We check whether the header has been explicitly set to 0 (disabled), which is worse than having no header at all.

Each header is weighted by importance. HSTS and CSP are worth 20 points each because they provide the broadest protection. X-Frame-Options and X-Content-Type-Options are worth 15 points apiece. X-XSS-Protection, Referrer-Policy, and Permissions-Policy are worth 10 points each. A site with all seven configured correctly scores 100 — and deserves it.

Actionable Recommendations

Start with the easy wins. Deploy X-Frame-Options, X-Content-Type-Options, Referrer-Policy, and Permissions-Policy today. These headers have simple, static values and will not break your site. That's 50 points on the ScanSentinel scale with near-zero effort.

Add HSTS. If your site already serves everything over HTTPS (which it should), adding HSTS takes one line of configuration. Start with a short max-age during testing, then increase to one year once you're confident.

Tackle CSP methodically. CSP is the hardest header to get right. Use Content-Security-Policy-Report-Only first. Collect violation reports. Iterate. Only switch to enforcing mode when your reports show zero legitimate violations over a monitoring period.

Don't stop at implementation — verify. Adding headers to your configuration file doesn't guarantee they're being served. Proxy servers, CDNs, load balancers, and application middleware can strip or override headers. After deploying, test with ScanSentinel (or manually with browser DevTools) to confirm the headers are actually reaching the client.

Re-check after every deployment. We've seen CSP headers disappear because someone updated a Lambda function and forgot to include the response headers configuration. Make header verification part of your CI/CD pipeline or post-deployment checklist.

Check your security headers now →