DoAide Code

Guides · Updated 2026-10-23

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'

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:

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

  1. Start from a preset close to your stack and list the third parties you actually load (DevTools → Network, filter by domain).
  2. Ship it as Content-Security-Policy-Report-Only with a report-uri. Nothing is blocked; violations are reported.
  3. Browse every page type for a few days and fix or allow each legitimate violation.
  4. 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:

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

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 — free

Frequently 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