Web Security Cheatsheet

Security Headers and Hardening

Use this Web Security reference while you build software engineering projects, review code, or refresh the syntax you reach for most.

The header reference

Response headers are cheap, browser-enforced defense in depth. Set them at the edge so every response gets them, including error pages and static files. Security Misconfiguration is A02:2025, and missing headers are a large part of it.

HeaderSane valueWhat it does
Content-Security-Policysee belowControls which resources may load and execute
Strict-Transport-Securitymax-age=63072000; includeSubDomainsForces HTTPS for the whole domain
X-Content-Type-OptionsnosniffStops MIME sniffing that turns text into HTML or script
Referrer-Policystrict-origin-when-cross-originLimits what leaks in Referer
Permissions-Policycamera=(), microphone=(), geolocation=()Denies powerful browser APIs
X-Frame-OptionsDENYLegacy anti-framing, superseded by frame-ancestors
Cross-Origin-Opener-Policysame-originSevers the window.opener link to cross-origin pages
Cross-Origin-Resource-Policysame-originBlocks cross-origin embedding of your resources
Cross-Origin-Embedder-Policyrequire-corpNeeded for cross-origin isolation, breaks unmarked third-party assets
Cache-Controlno-store on authenticated responsesKeeps private data out of shared and disk caches
Clear-Site-Data"cache", "cookies", "storage"Wipes client state on logout
Reporting-Endpointsdefault="https://.../reports"Where CSP and Permissions-Policy reports go

Deliberately absent: X-XSS-Protection is obsolete and was itself a vulnerability source, and Expect-CT is retired. Do not add either.

CSP directives

Content-Security-Policy is the largest and highest-value header. default-src is the fallback for most fetch directives, but several directives do not fall back to it, which is the usual reason a policy has a hole.

DirectivePurposeFalls back to default-src
default-srcFallback for fetch directives—
script-srcWhere scripts may load and execute fromyes
style-srcStylesheetsyes
img-srcImagesyes
connect-srcfetch, XHR, WebSocket, EventSourceyes
font-srcFontsyes
media-srcAudio and videoyes
worker-srcWorkers and service workersyes
object-src<object>, <embed>, set to 'none'yes
frame-srcWhat may be loaded into your framesyes
frame-ancestorsWho may frame you, anti-clickjackingno
base-uriRestricts <base href>, set to 'none'no
form-actionWhere forms may submitno
sandboxApplies iframe sandbox rules to the documentno
require-trusted-types-forEnforce Trusted Types at DOM sinksno
trusted-typesNames the allowed Trusted Types policiesno
upgrade-insecure-requestsRewrites http: subresource URLs to https:no
report-toReporting group name from Reporting-Endpointsno

Source keywords: 'self', 'none', 'unsafe-inline', 'unsafe-eval', 'strict-dynamic', 'unsafe-hashes', 'nonce-<value>', 'sha256-<digest>'. The single quotes are part of the syntax and omitting them silently changes the meaning to a hostname.

Content-Security-Policy:
  default-src 'self';
  script-src 'nonce-{RANDOM}' 'strict-dynamic' https: 'unsafe-inline';
  style-src 'self';
  img-src 'self' data: https:;
  connect-src 'self' https://api.example.com;
  object-src 'none';
  base-uri 'none';
  form-action 'self';
  frame-ancestors 'self';
  require-trusted-types-for 'script';
  upgrade-insecure-requests;
  report-to csp

Roll out with Content-Security-Policy-Report-Only first. The report-only header is never enforced, so you can read violations for a week and fix them before switching. Note that report-uri is deprecated in favor of report-to plus a Reporting-Endpoints header, and some browsers still only support the old one, so sending both during a transition is reasonable.

X-Frame-Options versus frame-ancestors

Both prevent clickjacking by controlling who may put your page in a frame. They overlap, and the difference matters.

X-Frame-OptionsCSP frame-ancestors
StandardDe facto, RFC 7034, effectively legacyCSP Level 2, the current mechanism
ValuesDENY, SAMEORIGIN (ALLOW-FROM is unsupported)'none', 'self', an origin list, wildcards
Multiple originsNot possibleYes
When both are sentframe-ancestors wins in browsers that support it—
X-Frame-Options: DENY
Content-Security-Policy: frame-ancestors 'none'

Send both. frame-ancestors is the real control, and X-Frame-Options covers old clients at no cost. Never rely on JavaScript frame-busting, it is bypassable.

Referrer-Policy and Permissions-Policy

Referrer-Policy controls how much of the current URL is sent in Referer on outbound requests. The default in modern browsers is already strict-origin-when-cross-origin, but set it explicitly.

ValueSends
no-referrerNothing, the most private
same-originFull URL to your own origin, nothing cross-origin
strict-originOrigin only, and nothing on an HTTPS to HTTP downgrade
strict-origin-when-cross-originFull URL same-origin, origin only cross-origin, nothing on downgrade
no-referrer-when-downgradeWeaker, leaks full paths cross-origin
unsafe-urlFull URL always, never use this

If your URLs contain identifiers, reset tokens, or search terms, use strict-origin or no-referrer, since even the origin-only default sends the path to same-origin destinations.

Permissions-Policy denies browser features. An empty allowlist () disables the feature everywhere including iframes, and self plus quoted origins grants it narrowly.

Permissions-Policy: camera=(), microphone=(), geolocation=(),
  payment=(), usb=(), serial=(), bluetooth=(), midi=(),
  display-capture=(), idle-detection=(), interest-cohort=()
Permissions-Policy: geolocation=(self "https://maps.example.com")

Deny everything you do not use. * and () may only appear alone, while self combines with a space-separated list of quoted origins.

Cross-origin isolation and CORS

The Cross-Origin-* trio governs how your document relates to other origins.

HeaderValueEffect
Cross-Origin-Opener-Policysame-originA page you open cannot reach back through window.opener, which blocks tabnabbing and cross-window leaks
Cross-Origin-Resource-Policysame-originOther origins cannot embed your responses, which mitigates side-channel resource leaks
Cross-Origin-Embedder-Policyrequire-corpEvery cross-origin subresource must opt in. Combined with COOP it gives cross-origin isolation, needed for SharedArrayBuffer and high-resolution timers.

CORS is not a security header, it relaxes the same-origin policy. Getting it wrong is a common misconfiguration.

MistakeWhy it is dangerous
Access-Control-Allow-Origin: * with Allow-Credentials: trueBrowsers reject this combination, and code that reflects the origin to work around it exposes authenticated data to every site
Reflecting Origin uncheckedEquivalent to allowing every origin, with credentials
An allowlist matched with startsWith/endsWithhttps://app.example.com.evil.tld passes a naive check
Allow-Origin: nullThe null origin is reachable from a sandboxed frame or a local file
Forgetting Vary: OriginA cache serves one origin's allowed response to another
// Correct: exact-match set, and Vary so caches stay correct
const ALLOWED = new Set(['https://app.example.com', 'https://admin.example.com'])
const origin = req.get('Origin')
if (origin && ALLOWED.has(origin)) {
  res.set('Access-Control-Allow-Origin', origin)
  res.set('Access-Control-Allow-Credentials', 'true')
  res.set('Vary', 'Origin')
}

Cache and logout hygiene

Authenticated pages in a shared or on-disk cache are a real leak, especially behind a corporate proxy or on a shared machine.

Cache-Control: no-store

no-store is the one that actually prevents storage. no-cache means "revalidate before reuse", which still writes to disk, and private only excludes shared caches. Use no-store for any response containing personal data, and let static assets be aggressively cacheable with a content hash in the filename.

On logout, clear client state as well as the server session.

Clear-Site-Data: "cache", "cookies", "storage"

Setting them, and verifying

Set headers once at the edge rather than per route, so a new endpoint inherits them.

add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header X-Frame-Options "DENY" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;

The always flag matters in nginx, because without it error responses do not get the header. Verify what is actually served rather than what the config says.

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

Other hardening in the same category as headers:

- Turn off directory listings, debug mode, stack traces, and default admin pages. - Remove version banners such as Server and X-Powered-By, which are free recon. - Return generic error pages, and log the detail with a request id server side. - Keep dependencies current and generate an SBOM. Software Supply Chain Failures is A03:2025, and an unpatched library is the most common way in. - Enforce HTTPS at the edge and redirect HTTP, then let HSTS take over. - Ship a /.well-known/security.txt so a finder knows where to report.

Further reading: the OWASP HTTP Security Response Headers Cheat Sheet and MDN HTTP headers.