Security Headers Worth Setting First

4 min read

Đọc bằng tiếng Việt →

Security headers are some of the cheapest hardening you can add to a site: a few lines of server configuration that instruct browsers to behave more strictly. They are not a substitute for safe code, but they limit the damage when something slips through. Here is a prioritised list, with the order we would roll them out and the one that needs the most care.

Quick wins that rarely break anything

X-Content-Type-Options

Setting nosniff stops browsers from guessing a file’s type from its contents, which prevents a malicious upload from being treated as a script. It only works properly if your server sends accurate Content-Type values, so check that first.

Strict-Transport-Security

HSTS tells browsers to use HTTPS for your domain for a stated period, without relying on a redirect from HTTP. The usual illustration from web.dev is a lifetime of one year in seconds. Start with a shorter period while you confirm every subdomain really serves HTTPS, and extend it once you are sure.

Framing controls

Clickjacking works by embedding your page in someone else’s frame. The older X-Frame-Options header accepts DENY or SAMEORIGIN. The CSP directive frame-ancestors does the same job with finer control, and MDN’s guidance is that frame-ancestors 'none' can replace the older header unless your pages must be embedded.

Cross-origin isolation headers

Cross-Origin-Opener-Policy isolates your window from pages that opened it, and Cross-Origin-Resource-Policy limits which sites can load your resources. They are recommended for sites in general, while Cross-Origin-Embedder-Policy matters mainly if you need features such as SharedArrayBuffer, because requiring embedded resources to opt in can break third-party content.

# nginx: a conservative starting set
add_header X-Content-Type-Options "nosniff" always;
add_header Strict-Transport-Security "max-age=31536000" always;
add_header Cross-Origin-Opener-Policy "same-origin-allow-popups" always;
add_header Cross-Origin-Resource-Policy "same-site" always;

Treat that block as a template to test, not a paste-and-forget answer. The resource policy in particular should match how your images and scripts are shared across subdomains.

Content-Security-Policy: the powerful one

CSP restricts what scripts and other resources a page may load or run, so injected code is blocked even if sanitisation fails. MDN is explicit that it complements, not replaces, escaping and sanitising input. A policy is a list of directives such as default-src 'self', each restricting a resource type.

The recommendation from both MDN and web.dev is a strict CSP based on nonces or hashes rather than long allowlists of domains. Allowlists are easy to get wrong, tend to include domains that host unsafe content, and become unwieldy with many third-party tags. A strict policy looks like this:

Content-Security-Policy:
  script-src 'nonce-{RANDOM}' 'strict-dynamic';
  object-src 'none';
  base-uri 'none';
  frame-ancestors 'none'
  • Use nonces when the server renders pages dynamically: a fresh unpredictable value for every response, added to each trusted script tag.
  • Use hashes for static pages, where the content of each inline script is fixed. Any edit to the script means recalculating its hash.
  • 'strict-dynamic' lets a trusted script load further scripts, which eases third-party integration, though it weakens protection if those scripts build tags from untrusted input.
  • Avoid 'unsafe-inline'; browsers ignore it when a nonce or hash is present anyway, but relying on it defeats the purpose.

Roll it out in report-only mode

Enforcing a new policy blind will break things. Send it first as Content-Security-Policy-Report-Only, collect the violation reports, fix what they reveal, then switch to enforcement. MDN describes reporting through a Reporting-Endpoints header with the report-to directive, and recommends keeping the deprecated report-uri alongside it while browser support catches up. Report-only policies cannot be delivered through a meta tag, which is one more reason to prefer real headers.

Reporting-Endpoints: csp="https://example.com/csp-reports"
Content-Security-Policy-Report-Only:
  script-src 'nonce-{RANDOM}' 'strict-dynamic';
  object-src 'none'; base-uri 'none';
  report-to csp

Refactor what the policy blocks

A strict policy stops inline event handlers such as onclick="...", javascript: URLs and eval(). Replace handlers with addEventListener calls and remove eval usage. For DOM-based injection, web.dev also describes Trusted Types, enabled with a CSP directive, which forces data through a sanitising policy before it reaches risky APIs like innerHTML.

A caveat on dates

The web.dev quick reference was last updated in 2021, and it notes that Safari did not support strict-dynamic at the time. Browser support moves, so verify current compatibility before relying on any single header, and note that web.dev defers Referrer-Policy to a separate article.

Where this fits on a platform you do not fully control

On hosted platforms you may only be able to set some headers, or none. In that case, focus on what you control: the scripts you include and the third-party tags you allow. If you want help planning a header rollout for a WordPress or custom site, our services are a good starting point.

Further reading

← All posts