iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Security Headers

HTTP security headers are the cheapest defense you can deploy — one block of middleware turns dozens of browser-side risks into errors. Strict-Transport-Security, Content-Security-Policy, X-Content-Type-Options, Referrer-Policy and Permissions-Policy are the modern starter kit.

CSP, HSTS, COOP/COEP, Permissions-Policy

EXAMPLE
// 1) Express middleware — helmet sets sensible defaults
import express from 'express';
import helmet from 'helmet';

const app = express();
app.use(helmet());
// One line wires up: CSP, HSTS, X-Content-Type-Options, X-DNS-Prefetch-Control,
// X-Download-Options, X-Frame-Options, Referrer-Policy, and more.

// Override what you actually need
app.use(helmet({
    contentSecurityPolicy: {
        useDefaults: true,
        directives: {
            'default-src':  ["'self'"],
            'script-src':   ["'self'", "'nonce-${RES_NONCE}'", 'https://cdn.example.com'],
            'style-src':    ["'self'", "'unsafe-inline'"],         // tighten later
            'img-src':      ["'self'", 'data:', 'https://cdn.example.com'],
            'font-src':     ["'self'", 'https://fonts.gstatic.com'],
            'connect-src':  ["'self'", 'https://api.example.com'],
            'frame-ancestors': ["'none'"],
            'base-uri':     ["'self'"],
            'form-action':  ["'self'"],
            'object-src':   ["'none'"],
            'upgrade-insecure-requests': [],
        },
    },
    crossOriginEmbedderPolicy: false,                          // enable only if you need it (worker isolation, SharedArrayBuffer)
}));

// 2) Strict-Transport-Security — force HTTPS
res.setHeader(
    'Strict-Transport-Security',
    'max-age=31536000; includeSubDomains; preload',
);
// max-age in seconds (1 year).  preload after you've submitted to https://hstspreload.org.
// CAUTION: long max-age + wrong cert breaks everyone.  Roll out with a short max-age first.

// 3) Content-Security-Policy — script execution allowlist
//   • script-src 'self' 'nonce-...' — disallow inline scripts unless they carry the nonce
//   • object-src 'none'             — kill <object>, <embed>, <applet>
//   • frame-ancestors 'none'        — equivalent of X-Frame-Options: DENY
//   • upgrade-insecure-requests     — auto-rewrite http: to https:
//
// Inline scripts in templates must use the per-request nonce:
// <script nonce="\${nonce}">window.config = …;</script>

import crypto from 'node:crypto';
app.use((req, res, next) => {
    res.locals.cspNonce = crypto.randomBytes(16).toString('base64');
    next();
});

// 4) X-Content-Type-Options: nosniff — stop MIME-type sniffing
res.setHeader('X-Content-Type-Options', 'nosniff');

// 5) Referrer-Policy — limit referrer leak
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
// Good default: full URL same-origin, only the origin cross-origin, nothing on HTTP downgrade.

// 6) Permissions-Policy — opt into / out of browser features
res.setHeader(
    'Permissions-Policy',
    [
        'camera=()',
        'microphone=()',
        'geolocation=(self)',
        'payment=(self)',
        'usb=()',
        'autoplay=(self)',
        'interest-cohort=()',                  // disable FLoC
    ].join(', '),
);

// 7) X-Frame-Options: DENY — legacy clickjacking defense
res.setHeader('X-Frame-Options', 'DENY');
// CSP frame-ancestors is more granular; ship both for older browsers.

// 8) Cross-Origin-Opener-Policy / Cross-Origin-Embedder-Policy — origin isolation
res.setHeader('Cross-Origin-Opener-Policy',   'same-origin');
res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp');
// COOP keeps pop-up attacks (window.opener tampering) from working.
// COEP gates loading of cross-origin resources unless they opt in.
// Needed for SharedArrayBuffer / cross-origin isolated APIs.

// 9) Cache-Control on auth/PII responses
res.setHeader('Cache-Control', 'no-store');
res.setHeader('Pragma',         'no-cache');
res.setHeader('Expires',        '0');
// Stops shared proxies from caching user-specific responses.

// 10) CORS — narrow allowlist, never wildcard with credentials
const ALLOWED = new Set([
    'https://app.example.com',
    'https://admin.example.com',
]);
app.use((req, res, next) => {
    const origin = req.get('origin');
    if (origin && ALLOWED.has(origin)) {
        res.setHeader('Access-Control-Allow-Origin', origin);
        res.setHeader('Access-Control-Allow-Credentials', 'true');
        res.setHeader('Vary', 'Origin');
    }
    if (req.method === 'OPTIONS') {
        res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE');
        res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-CSRF-Token');
        res.setHeader('Access-Control-Max-Age', '600');
        return res.sendStatus(204);
    }
    next();
});

// 11) Static-site / CDN — set headers there too
// Cloudflare / Fastly / nginx — apply the same set at the edge so it covers static assets.
// Example nginx:
// add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
// add_header X-Content-Type-Options nosniff always;
// add_header Referrer-Policy "strict-origin-when-cross-origin" always;
// add_header Content-Security-Policy "default-src 'self'; ..." always;

// 12) Report-only mode for rollout
res.setHeader(
    'Content-Security-Policy-Report-Only',
    "default-src 'self'; script-src 'self'; report-to csp-endpoint",
);
res.setHeader('Report-To', JSON.stringify({
    group: 'csp-endpoint',
    max_age: 10886400,
    endpoints: [{ url: 'https://api.example.com/csp-report' }],
}));
// Read reports for a week or two, fix the violations, THEN switch to enforce.

// 13) Validate from outside
//   • https://securityheaders.com — grade your endpoints
//   • https://csp-evaluator.withgoogle.com — CSP common-mistake checker
//   • https://hstspreload.org — only after you're sure about your HSTS rollout
//   • Browser devtools → Network → Headers — verify on every endpoint

// 14) CI guard — fail the build if headers regress
import request from 'supertest';
import { test, expect } from 'vitest';

test('security headers present on every endpoint', async () => {
    for (const path of ['/', '/login', '/api/health']) {
        const r = await request(app).get(path);
        expect(r.headers['strict-transport-security']).toMatch(/max-age=/);
        expect(r.headers['x-content-type-options']).toBe('nosniff');
        expect(r.headers['content-security-policy']).toMatch(/script-src/);
        expect(r.headers['referrer-policy']).toBeTruthy();
        expect(r.headers['permissions-policy']).toMatch(/camera=\(\)/);
    }
});

// 15) Common bugs
//   • CSP allows 'unsafe-inline' on script-src → XSS becomes easy to weaponise
//   • HSTS with includeSubDomains + a subdomain without HTTPS → outage
//   • Wildcard CORS with Allow-Credentials → standards-violation; browsers ignore it (good) but it signals bad config
//   • Missing Vary: Origin → CDN serves the wrong CORS headers to the wrong origin
//   • X-XSS-Protection still on → it's deprecated; rely on CSP, drop the header
//   • COEP: require-corp without sending CORP headers from CDN → assets fail to load

Why it matters

Ship a baseline header set on day one — HSTS, CSP, nosniff, Referrer-Policy, Permissions-Policy — and use Content-Security-Policy-Report-Only for a week before enforcement so you find the inline scripts and third-party loads that need fixing. Once enforced, it’s the highest-leverage defense per line of config in your whole stack.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
// Modern defaults
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
Content-Security-Policy: default-src 'self'; …
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=()
Cross-Origin-Opener-Policy: same-origin
Try it Yourself »

Exercise

Strict transport security header name.

: max-age=63072000; includeSubDomains; preload

Discussion

Loading…