SSentinel Board
GuidesPricingOpen dashboard ↗

WEB SECURITY

HTTP security headers, explained

Obsidian Orb InnovationsUpdated 9 September 2026~7 min read

"Add security headers" is the most common recommendation in any web security scan, and one of the most commonly half-implemented. Each header defends against a specific attack, and a couple of them will break your site if you set them carelessly. Here is what each one does, how to check what you are already sending, and a starting configuration you can adapt.

First, see what you send today

Before changing anything, look at your current response headers:

curl -sSI https://yoursite.example.com/ | grep -iE \
  'strict-transport|content-security|x-frame|x-content-type|referrer|permissions'

Whatever is missing from that output is a gap. Now, header by header.

Strict-Transport-Security (HSTS)

Tells the browser to only ever contact your site over HTTPS, even if a user types http:// or clicks an old link. This closes the window where a first, plaintext request can be intercepted and downgraded.

Strict-Transport-Security: max-age=31536000; includeSubDomains
HSTS is sticky. Once a browser sees it, it refuses plain HTTP for max-age seconds — so do not send it until HTTPS works everywhere, including every subdomain if you add includeSubDomains. Start with a short max-age while you confirm nothing breaks, then raise it.

Content-Security-Policy (CSP)

The most powerful header here, and the fiddliest. CSP tells the browser which sources of scripts, styles, images, and other resources are allowed to load — so even if an attacker injects a <script> tag, the browser refuses to run it. It is the strongest defense against cross-site scripting there is.

A strict starting point for a site that loads only its own assets:

Content-Security-Policy: default-src 'self';
  img-src 'self' data:;
  style-src 'self' 'unsafe-inline';
  script-src 'self';
  base-uri 'self';
  frame-ancestors 'none'

The hard part is 'unsafe-inline'. Every inline <script> and inline event handler (onclick="...") is blocked by script-src 'self', which is exactly the point — but it means a page with inline scripts will silently stop working. Move that JavaScript into external files. Inline styles are more often unavoidable (many UIs set style="width:..." dynamically), which is why the example allows 'unsafe-inline' for styles but not scripts.

CSP fails silently — a blocked resource just does not load, with no visible error unless you are watching the console. Add a report-uri or test in report-only mode (Content-Security-Policy-Report-Only) before enforcing.

X-Frame-Options

Stops other sites from loading yours inside an <iframe>, which is how clickjacking works — an invisible frame of your site laid over a decoy the victim thinks they are clicking. DENY blocks all framing:

X-Frame-Options: DENY

The modern equivalent is the CSP frame-ancestors 'none' directive, which is more flexible; sending both covers older browsers too.

X-Content-Type-Options

Stops the browser from "sniffing" a response's content type and second-guessing your declared Content-Type. Without it, a file you serve as text could be interpreted as script. There is only one value, and every site should send it:

X-Content-Type-Options: nosniff

Referrer-Policy

Controls how much of the current URL is sent in the Referer header when a user navigates away. This matters when your URLs contain anything sensitive — tokens, IDs, search terms. A good default sends the full URL within your own site but only the origin to other sites:

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

Permissions-Policy

Declares which browser features — camera, microphone, geolocation, and others — the page is allowed to use. Disabling what you do not need shrinks the damage a script injection could do:

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

A starting configuration

Putting it together, here is a reasonable baseline for a static or server-rendered site. On Netlify or Cloudflare Pages this goes in a _headers file; on nginx or Apache it maps to add_header / Header set directives:

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

Adapt the CSP to your real dependencies: if you load fonts, analytics, or a payment widget from another domain, you must add those hosts to the relevant directive or the browser will block them. That is the whole point of the header — so add hosts deliberately, one at a time, rather than loosening it to *.

When you are done, re-run the curl check from the top of this page against your live site to confirm every header is actually being sent.

SENTINEL BOARD

Check headers as part of a full API scan

Sentinel Board's scanner flags missing and weak security headers automatically, alongside broken access control, injection, CORS, and TLS checks — then scores your posture and imports the findings into a local dashboard. If you are hardening an API, headers are one line item of many; the toolkit tracks them all in one place. One-time $49, no subscription, runs entirely on your machine.

See what's included →
← All guides API security testing checklist Flutter app security testing
SSentinel Board
GuidesTermsPrivacyRefunds
© 2026 Obsidian Orb Innovations