CSP, CORS and security headers, explained
Most web security incidents are not exotic zero-days. They are an injected script that a Content Security Policy would have blocked, an API that echoes any origin with credentials, or a password table hashed with plain SHA-256. All three are fixed with configuration you can write in an afternoon.
This guide explains the headers that matter, in the order you should add them, with the mistakes that trip teams up. Each section links a free tool that does the fiddly part for you: the CSP generator, the CORS tester, the nginx config generator and the bcrypt generator.
The baseline every site should send
Four headers cost nothing and close whole classes of attacks:
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Content-Security-Policy: frame-ancestors 'none'
- HSTS tells the browser to use HTTPS for your domain for a year, so a network attacker can’t downgrade the first request. Only send it over HTTPS, and only add
preloadonce every subdomain supports HTTPS. - nosniff stops the browser guessing a file is a script when the server says it is text.
- Referrer-Policy keeps full URLs — which can contain tokens — from leaking to other sites.
- frame-ancestors stops other sites framing yours for clickjacking. It replaces
X-Frame-Options, though sending both is harmless for old browsers.
In nginx there is one trap: a location block with its own add_header silently drops every add_header inherited from the server block. The nginx config generator repeats the security headers in those locations so they are never lost.
Content Security Policy: what it blocks
A CSP is an allow-list of where each kind of resource may come from. If an attacker injects <script src="https://evil.example/x.js"> into your page, the browser refuses to load it because that host is not in script-src.
Content-Security-Policy:
default-src 'self';
script-src 'self' https://www.googletagmanager.com;
style-src 'self' https://fonts.googleapis.com;
font-src 'self' https://fonts.gstatic.com;
img-src 'self' data:;
connect-src 'self' https://api.example.com;
object-src 'none';
base-uri 'self';
frame-ancestors 'none';
upgrade-insecure-requests
default-src is the fallback for every fetch directive you don’t list. base-uri, form-action and frame-ancestors do not fall back to it, which is why a policy with only default-src still lets an injected <base> tag redirect your relative script URLs.
The directive that matters most is script-src. Two values undo most of its protection: 'unsafe-inline' (any inline <script> or onclick runs — exactly what XSS injects) and broad sources like https: or *. Paste your current policy into the audit tab of the CSP generator to see which of these you have.
Nonces and hashes instead of unsafe-inline
You rarely need 'unsafe-inline'. Two safer ways allow the inline code you wrote and nothing else:
- Nonce — the server generates a random value on every response, puts it in the header (
script-src 'nonce-R4nd0m…') and on each tag (<script nonce="R4nd0m…">). An attacker can’t guess it. Never reuse a nonce or put it in a cached page. - Hash — for a fixed inline script, add
'sha256-…'of its exact contents. Any change, even whitespace, changes the hash.
Add 'strict-dynamic' next to a nonce and scripts loaded by your trusted scripts are allowed too, which makes tag managers and bundlers with dynamic imports work without listing every host. The generator’s Strict (nonce) preset builds this policy, and its hash helper computes the sha256 source for an inline block.
Rolling out CSP without breaking production
- Start from a preset close to your stack and list the third parties you actually load (DevTools → Network, filter by domain).
- Ship it as
Content-Security-Policy-Report-Onlywith areport-uri. Nothing is blocked; violations are reported. - Browse every page type for a few days and fix or allow each legitimate violation.
- Switch the header name to
Content-Security-Policy. Keep the report endpoint to catch regressions.
A <meta http-equiv="Content-Security-Policy"> tag works for static hosting where you can’t set headers, but browsers ignore frame-ancestors, report-uri and sandbox in it — the generator leaves those out of the meta version for you.
CORS: why the browser blocks your API call
CORS is not a security feature you add; it is the controlled relaxation of the same-origin policy. By default a page on https://app.example.com can send a request to https://api.example.com but cannot read the response. The API opts in by answering with Access-Control-Allow-Origin.
Some requests are preflighted: before the real request the browser sends OPTIONS asking permission. That happens for methods other than GET, HEAD and POST, for headers such as Authorization or X-Api-Key, and for a Content-Type other than form, multipart or text/plain — so every fetch that posts JSON is preflighted.
The rules people break most often:
Access-Control-Allow-Origin: *cannot be combined with credentials (cookies). Echo the exact origin, sendAccess-Control-Allow-Credentials: trueand addVary: Origin.- The header must contain one origin. A comma-separated list is invalid — check the request’s
Originagainst your allow-list and echo the match. Access-Control-Allow-Headers: *never coversAuthorization; name it explicitly.- The preflight must return 2xx. An auth middleware that answers
OPTIONSwith 401 breaks every preflighted request. - Custom response headers such as
X-Total-Countare invisible to JavaScript unless listed inAccess-Control-Expose-Headers.
Paste the response headers from DevTools into the CORS tester and it runs these checks step by step, tells you which one fails, and writes the fixed config for nginx, Express, FastAPI or Apache. It never sends a request, so it works for localhost and private APIs.
Storing passwords: bcrypt, cost and the 72-byte limit
Headers protect the browser; the database needs its own defence. Never store passwords encrypted or with a fast hash like SHA-256 — a GPU tries billions of those per second. Use a deliberately slow, salted password hash: Argon2id for new systems, or bcrypt, which every language and framework supports.
$2b$12$R9h/cIPz0gi.URNNX3kh2OPST9/PgBkqquzi.Ss7KIUgO2t0jWMUW
│ │ └ 22-char salt ─────┘└ 31-char hash ──────────────┘
│ └ cost: 2^12 rounds
└ version
- Pick a cost where one hash takes about 250 ms on your server — usually 10 to 12 today — and raise it as hardware gets faster; rehash on next login.
- bcrypt only uses the first 72 bytes of the password. Long passphrases, especially with multi-byte characters, can be silently truncated.
$2a$,$2b$and$2y$are labels for the same algorithm; libraries verify all three.
The bcrypt generator hashes in your browser and times each cost factor, so you can see what a step up costs before you change production.
Build or audit your Content Security Policy — freeFrequently asked questions
What is the best Content Security Policy for a website?
A nonce-based policy: script-src 'nonce-…' 'strict-dynamic', object-src 'none', base-uri 'none' and frame-ancestors 'none'. It blocks injected scripts without listing every third-party host. Roll it out in Report-Only mode first.
How do I fix “No Access-Control-Allow-Origin header is present”?
Change the server, not the frontend: return Access-Control-Allow-Origin with your page’s exact origin (or * for public, cookie-less APIs), and answer the OPTIONS preflight with 2xx plus Allow-Methods and Allow-Headers.
Is CORS a security feature?
CORS relaxes the browser’s same-origin policy; it does not protect your API from curl, scripts or other servers. Authenticate and authorise every request on the server regardless of CORS.
Is bcrypt still safe in 2026?
Yes, at a cost of 10–12 or higher. Argon2id is the stronger modern choice for new systems, but a well-configured bcrypt is far better than any fast hash such as SHA-256 or MD5.
More guides
- How to review code with AI, free and without sign-up
- How to convert code between languages with AI (Python, Java, JavaScript and more)
- How to generate unit tests with AI: from zero tests to a real suite
- The best free code review tools in 2026
- AI code tools for beginners: learn faster without learning less
- 10 free AI code tools every developer needs in 2026
- DoAide Code: the free alternative to GitHub Copilot
- How to review code with AI for free: workflows that stick
- How to write clean code in 2026
- Best practices for code documentation
- The best free online code formatter in 2026
- Online regex tester and builder: write regular expressions that actually work
- Free online SQL formatter and validator
- Code snippet sharing for developers: stop pasting code into chat
- How to generate a JSON Schema from a JSON sample
- The flexbox cheat sheet: every property and what it actually moves
- Meta tags for SEO: what your pages actually need in 2026
- UUID v4 vs v7: which one should you use?
- CSS Grid explained: tracks, fr, minmax and template areas
- Linux file permissions and chmod, explained
- How to mock a REST API for frontend development
- YAML, JSON, CSV and XML: converting data and config files safely