ScanBastion Join Early Access

Learn

HTTP security headers explained for agency developers

Security headers are the highest-return fix on most client sites: a few lines of server or CDN configuration that make whole classes of attack harder, with no application changes. They are also where a confident developer most often breaks a site in production. This guide covers the eight that matter, a safe starting value for each, and the two to treat with care.

Where headers are set

Response headers can be added by the application, the web server (nginx, Apache, IIS), a reverse proxy or a CDN edge. For an agency maintaining many sites, the server or CDN layer is usually best: one configuration block, applied to every response, under version control. Whichever layer you choose, set each header in one place. Duplicate headers with different values are a common cause of "we set it but the scanner still complains" and of browsers applying the stricter of two policies you did not intend.

The eight headers

1. Strict-Transport-Security (HSTS)

Tells the browser to use HTTPS for this host for the stated period, even if a user types http:// or follows an old link. Blocks protocol-downgrade attacks and stops cookies leaking over plain HTTP.

Starting value: max-age=31536000 (one year). Prerequisite: every hostname the header covers must serve HTTPS correctly, now and for the next year. Do not add includeSubDomains until you have checked every subdomain, including ones for email, staging and third-party services with CNAMEs; and do not add preload unless you understand that it is effectively permanent. HSTS is the first of the two headers that break sites.

2. Content-Security-Policy (CSP)

Declares which sources of scripts, styles, images, frames and connections the page may use. A good CSP turns a cross-site scripting bug from a full compromise into a blocked script. It is the most valuable header and the hardest to get right.

Starting value: none, deployed blindly. Start with Content-Security-Policy-Report-Only carrying a policy such as default-src 'self'; img-src 'self' data: https:; script-src 'self'; style-src 'self' 'unsafe-inline'; frame-ancestors 'none'; base-uri 'self'; form-action 'self', watch the browser console across the site's real pages, add the sources that are legitimately needed, then switch to enforcing. Inline scripts are the usual obstacle; the fix is nonces or hashes, or moving the scripts to files. CSP is the second header that breaks sites, and it breaks them silently for visitors while looking fine to the developer whose browser has an extension injecting things anyway.

3. X-Frame-Options and frame-ancestors

Prevents the site being embedded in a frame on another origin, which defeats clickjacking. frame-ancestors in CSP is the modern form; X-Frame-Options covers older browsers.

Starting value: X-Frame-Options: DENY plus frame-ancestors 'none' in the CSP. If a client legitimately embeds the site somewhere, use SAMEORIGIN or list the specific origins in frame-ancestors.

4. X-Content-Type-Options

Stops the browser guessing a file's type from its content. Without it, an uploaded file served as text can be executed as script.

Starting value: nosniff. There is no downside; set it everywhere.

5. Referrer-Policy

Controls how much of the current URL is sent to the next site when a user clicks a link or a resource loads. Full URLs can carry session tokens, search terms and internal paths.

Starting value: strict-origin-when-cross-origin. It sends the full URL to your own pages and only the origin to others, and it is the default in current browsers; setting it explicitly covers older ones and makes the intent visible.

6. Permissions-Policy

Switches off browser features the site does not use, for the page and for anything it embeds. A compromised ad script cannot turn on the camera if the policy forbids it.

Starting value: camera=(), microphone=(), geolocation=(), payment=(), extended with whatever else the site does not need. Harmless to set; low impact unless the site embeds third-party content.

7. Cross-Origin-Opener-Policy

Isolates the page's browsing context from pages it opens or that opened it, which blocks a class of cross-window attacks and is required for some newer browser features.

Starting value: same-origin. Test any OAuth pop-up or payment flow that relies on window.opener; those may need same-origin-allow-popups.

8. Cookie attributes

Not a header of its own, but the attributes on every Set-Cookie decide whether a session can be stolen. Secure keeps the cookie off plain HTTP. HttpOnly keeps it away from scripts, so a cross-site scripting bug cannot read it. SameSite limits whether it is sent with cross-site requests, which blunts cross-site request forgery.

Starting value: Secure; HttpOnly; SameSite=Lax on session cookies. These are set by the application or framework rather than the web server; most frameworks have one setting for it.

One to stop setting

X-XSS-Protection controlled a browser filter that no current browser ships. Setting it does nothing in modern browsers and, in some old versions, the filter it enabled introduced vulnerabilities of its own. Scanners that still demand it are out of date; a good one will not flag its absence.

A deployment routine that does not break sites

  1. Set the four safe headers first: X-Content-Type-Options, Referrer-Policy, Permissions-Policy, X-Frame-Options. Deploy. Nothing will break.
  2. Add HSTS with a short max-age such as 300 for a day, confirm every hostname works over HTTPS, then raise it to a year.
  3. Run CSP in report-only mode for at least a week of normal traffic. Fix the reports. Then enforce.
  4. Fix cookie attributes in the application. Test log-in, log-out and any cross-site flows such as payment callbacks, which SameSite=Strict can break and Lax usually does not.
  5. Rescan. A headers check takes a minute and gives the client a before-and-after.

Explaining the work to a client

Clients rarely care about header names. They do care that "the browser now refuses to load the site insecurely", "a bug in a third-party script can no longer read customer sessions" and "the site cannot be embedded in a fake page to trick users". Translate each header into the thing it prevents, show the scan results before and after, and put the configuration in the client's repository so the next agency inherits it.