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.
| Header | Sane value | What it does |
|---|---|---|
Content-Security-Policy | see below | Controls which resources may load and execute |
Strict-Transport-Security | max-age=63072000; includeSubDomains | Forces HTTPS for the whole domain |
X-Content-Type-Options | nosniff | Stops MIME sniffing that turns text into HTML or script |
Referrer-Policy | strict-origin-when-cross-origin | Limits what leaks in Referer |
Permissions-Policy | camera=(), microphone=(), geolocation=() | Denies powerful browser APIs |
X-Frame-Options | DENY | Legacy anti-framing, superseded by frame-ancestors |
Cross-Origin-Opener-Policy | same-origin | Severs the window.opener link to cross-origin pages |
Cross-Origin-Resource-Policy | same-origin | Blocks cross-origin embedding of your resources |
Cross-Origin-Embedder-Policy | require-corp | Needed for cross-origin isolation, breaks unmarked third-party assets |
Cache-Control | no-store on authenticated responses | Keeps private data out of shared and disk caches |
Clear-Site-Data | "cache", "cookies", "storage" | Wipes client state on logout |
Reporting-Endpoints | default="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.
| Directive | Purpose | Falls back to default-src |
|---|---|---|
default-src | Fallback for fetch directives | — |
script-src | Where scripts may load and execute from | yes |
style-src | Stylesheets | yes |
img-src | Images | yes |
connect-src | fetch, XHR, WebSocket, EventSource | yes |
font-src | Fonts | yes |
media-src | Audio and video | yes |
worker-src | Workers and service workers | yes |
object-src | <object>, <embed>, set to 'none' | yes |
frame-src | What may be loaded into your frames | yes |
frame-ancestors | Who may frame you, anti-clickjacking | no |
base-uri | Restricts <base href>, set to 'none' | no |
form-action | Where forms may submit | no |
sandbox | Applies iframe sandbox rules to the document | no |
require-trusted-types-for | Enforce Trusted Types at DOM sinks | no |
trusted-types | Names the allowed Trusted Types policies | no |
upgrade-insecure-requests | Rewrites http: subresource URLs to https: | no |
report-to | Reporting group name from Reporting-Endpoints | no |
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-Options | CSP frame-ancestors | |
|---|---|---|
| Standard | De facto, RFC 7034, effectively legacy | CSP Level 2, the current mechanism |
| Values | DENY, SAMEORIGIN (ALLOW-FROM is unsupported) | 'none', 'self', an origin list, wildcards |
| Multiple origins | Not possible | Yes |
| When both are sent | frame-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.
| Value | Sends |
|---|---|
no-referrer | Nothing, the most private |
same-origin | Full URL to your own origin, nothing cross-origin |
strict-origin | Origin only, and nothing on an HTTPS to HTTP downgrade |
strict-origin-when-cross-origin | Full URL same-origin, origin only cross-origin, nothing on downgrade |
no-referrer-when-downgrade | Weaker, leaks full paths cross-origin |
unsafe-url | Full 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.
| Header | Value | Effect |
|---|---|---|
Cross-Origin-Opener-Policy | same-origin | A page you open cannot reach back through window.opener, which blocks tabnabbing and cross-window leaks |
Cross-Origin-Resource-Policy | same-origin | Other origins cannot embed your responses, which mitigates side-channel resource leaks |
Cross-Origin-Embedder-Policy | require-corp | Every 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.
| Mistake | Why it is dangerous |
|---|---|
Access-Control-Allow-Origin: * with Allow-Credentials: true | Browsers reject this combination, and code that reflects the origin to work around it exposes authenticated data to every site |
Reflecting Origin unchecked | Equivalent to allowing every origin, with credentials |
An allowlist matched with startsWith/endsWith | https://app.example.com.evil.tld passes a naive check |
Allow-Origin: null | The null origin is reachable from a sandboxed frame or a local file |
Forgetting Vary: Origin | A 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.