# HTTP Security Headers: A Complete Implementation Guide

> Learn how to implement HTTP security headers — CSP, HSTS, X-Frame-Options, and CORS — with configuration examples for Nginx, Apache, and Node.js.

- URL: https://www.browserutils.dev/blog/security-headers-guide
- Published: 2026-07-27
- Updated: 2026-07-02

---

HTTP security headers are your first line of defense against many common web attacks. They instruct browsers to enforce security policies — blocking XSS, preventing clickjacking, controlling resource loading, and enforcing HTTPS. Yet most websites are missing critical headers or have them misconfigured.

This guide covers each important security header with practical implementation examples.

## Content-Security-Policy (CSP)

CSP is the most powerful (and most complex) security header. It tells the browser exactly which resources are allowed to load and execute on your page, effectively shutting down most XSS attacks.

### Basic Policy

```
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'
```

This policy says:
- **default-src 'self'** — by default, only load resources from the same origin
- **script-src 'self'** — only execute scripts from the same origin (no inline scripts, no eval)
- **style-src 'self' 'unsafe-inline'** — styles from same origin plus inline styles
- **img-src 'self' data: https:** — images from same origin, data URIs, and any HTTPS source
- **font-src 'self'** — fonts from same origin only

### Common Directives

| Directive | Controls |
|-----------|---------|
| `default-src` | Fallback for all resource types |
| `script-src` | JavaScript |
| `style-src` | CSS |
| `img-src` | Images |
| `font-src` | Fonts |
| `connect-src` | XHR, fetch, WebSocket connections |
| `frame-src` | iframes |
| `media-src` | Audio and video |
| `object-src` | Plugins (Flash, Java) |
| `form-action` | Form submission targets |
| `frame-ancestors` | Who can embed this page (replaces X-Frame-Options) |

### Source Values

| Value | Meaning |
|-------|---------|
| `'self'` | Same origin |
| `'none'` | Block everything |
| `'unsafe-inline'` | Allow inline scripts/styles (weakens CSP significantly) |
| `'unsafe-eval'` | Allow eval() and similar |
| `'nonce-{random}'` | Allow specific inline scripts with matching nonce |
| `'strict-dynamic'` | Trust scripts loaded by already-trusted scripts |
| `https:` | Any HTTPS source |
| `data:` | Data URIs |
| `*.example.com` | Any subdomain of example.com |

### Nonce-Based CSP (Recommended)

Instead of allowing all inline scripts with `'unsafe-inline'`, use nonces:

```html
<!-- Server generates a random nonce per request -->
<script nonce="abc123def456">
  // This script is allowed because the nonce matches
  console.log("trusted");
</script>
```

```
Content-Security-Policy: script-src 'nonce-abc123def456' 'strict-dynamic'
```

`'strict-dynamic'` propagates trust to scripts loaded by the nonced script, so you do not need to whitelist CDN domains individually.

### Report-Only Mode

Test your CSP without breaking anything:

```
Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-reports
```

Violations are reported but not blocked. Deploy in report-only mode first, analyze violations, then switch to enforcing mode.

Building a correct CSP from scratch is tedious and error-prone. Our [CSP Header Generator](/tools/csp-header-generator) walks you through each directive with explanations, generates the header value, and validates it for common mistakes.

## Strict-Transport-Security (HSTS)

HSTS tells browsers to only connect via HTTPS, preventing SSL stripping attacks:

```
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
```

- **max-age=31536000** — enforce HTTPS for one year (in seconds)
- **includeSubDomains** — apply to all subdomains
- **preload** — eligible for browser HSTS preload lists (built into the browser)

**Warning:** HSTS is sticky. Once set, browsers will refuse HTTP connections for `max-age` duration. If you lose your SSL certificate or need to serve HTTP, you cannot easily undo it. Start with a short `max-age` (e.g., 300 seconds) and increase gradually.

### The Preload List

Adding your domain to the HSTS preload list (hstspreload.org) means browsers will enforce HTTPS for your domain even on the very first visit — before they have seen your HSTS header. This prevents attacks on the first connection but is essentially permanent. Remove your domain requires browser updates to propagate.

## X-Frame-Options

Prevents your site from being embedded in iframes on other sites (clickjacking protection):

```
X-Frame-Options: DENY
```

Values:
- `DENY` — never allow framing
- `SAMEORIGIN` — allow framing by the same origin
- `ALLOW-FROM https://trusted.com` — allow framing by a specific origin (deprecated, not supported by modern browsers)

**CSP replacement:** `frame-ancestors` in CSP is more flexible and replaces X-Frame-Options:

```
Content-Security-Policy: frame-ancestors 'self' https://trusted.com
```

Use both for backward compatibility with older browsers.

## X-Content-Type-Options

Prevents MIME type sniffing:

```
X-Content-Type-Options: nosniff
```

Without this header, browsers may interpret a file differently from its declared `Content-Type`. An attacker could upload a file with a `.jpg` extension containing JavaScript, and the browser might execute it. `nosniff` forces the browser to respect the declared type.

This header has only one valid value. Always include it.

## Referrer-Policy

Controls how much URL information is sent in the `Referer` header when navigating away from your site:

```
Referrer-Policy: strict-origin-when-cross-origin
```

Common values:

| Value | Behavior |
|-------|----------|
| `no-referrer` | Never send referrer |
| `origin` | Send only the origin (no path) |
| `strict-origin` | Send origin on HTTPS-to-HTTPS, nothing on HTTPS-to-HTTP |
| `strict-origin-when-cross-origin` | Full URL for same-origin, origin for cross-origin, nothing for downgrade |
| `unsafe-url` | Always send full URL (not recommended) |

`strict-origin-when-cross-origin` is the recommended default — it provides referrer information for same-origin requests (useful for analytics) while protecting path information on cross-origin requests.

## Permissions-Policy

Controls which browser features your site can use:

```
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()
```

Empty parentheses `()` disable the feature entirely. Other options:

```
Permissions-Policy: camera=(self), geolocation=(self "https://maps.example.com")
```

This prevents embedded iframes from accessing sensitive browser APIs unless explicitly allowed.

## CORS Headers

Cross-Origin Resource Sharing (CORS) headers control which origins can make requests to your API:

```
Access-Control-Allow-Origin: https://frontend.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Max-Age: 86400
```

### Common CORS Mistakes

**`Access-Control-Allow-Origin: *` with credentials:** The wildcard `*` does not work with `credentials: 'include'`. You must specify the exact origin.

**Reflecting the Origin header without validation:** Some servers echo back whatever origin the request sends. This is effectively the same as `*` and defeats the purpose of CORS.

**Missing preflight handling:** Browsers send an OPTIONS request before certain cross-origin requests. Your server must handle OPTIONS and return the appropriate CORS headers.

For testing CORS configurations, our [CORS Tester](/tools/cors-tester) sends requests to your API and shows exactly which CORS headers are returned, helping you verify that your configuration is correct. For a deeper look at how CORS works, see our [CORS explained](/blog/cors-explained) guide.

## Implementation

### Nginx

```nginx
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' https: data:;" always;
```

### Apache

```apache
Header always set X-Frame-Options "SAMEORIGIN"
Header always set X-Content-Type-Options "nosniff"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
Header always set Permissions-Policy "camera=(), microphone=(), geolocation=()"
```

### Node.js (Express)

```javascript
const helmet = require('helmet');
app.use(helmet());

// Or manually:
app.use((req, res, next) => {
  res.setHeader('X-Frame-Options', 'SAMEORIGIN');
  res.setHeader('X-Content-Type-Options', 'nosniff');
  res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
  res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
  next();
});
```

## Checking Your Headers

After implementing security headers, verify them. Our [Security Headers Checker](/tools/security-headers-checker) analyzes any URL and reports which security headers are present, which are missing, and whether the values are configured correctly. It provides specific recommendations for any gaps.

## Implementation Checklist

| Header | Priority | Difficulty |
|--------|----------|-----------|
| X-Content-Type-Options | High | Trivial |
| X-Frame-Options | High | Trivial |
| Strict-Transport-Security | High | Easy (if already HTTPS) |
| Referrer-Policy | Medium | Easy |
| Permissions-Policy | Medium | Easy |
| Content-Security-Policy | High | Complex |
| CORS | Situational | Medium |

Start with the easy headers — X-Content-Type-Options, X-Frame-Options, and Referrer-Policy can be added in minutes with zero risk. Then add HSTS (start with a low max-age). Finally, tackle CSP in report-only mode before enforcing.

Security headers are not a silver bullet, but they significantly raise the bar for attackers. The effort to implement them is small compared to the protection they provide.