# HTTP Security Headers Every Web Application Needs: Node.js, Express, and Nginx

> Set the essential HTTP security headers in Node.js, Express, and Nginx — Strict-Transport-Security, X-Frame-Options, CORP, COOP, and more with practical config examples.

- **Published:** 2025-06-22
- **Author:** Clixo
- **Reading time:** 5 min read
- **Tags:** http-headers, nodejs, express, nginx, web-security, hardening
- **Canonical URL:** https://clixo.sh/blog/http-security-headers-nodejs-express-nginx

Security headers are one of the fastest, highest-leverage improvements you can make to a web application's security posture. They require no changes to application logic, take minutes to configure, and address a meaningful set of browser-side attacks. Yet they are routinely missing from production deployments.

This guide covers the headers that matter, what each one actually does, and how to set them in Express and Nginx.

## Why HTTP Security Headers Matter

Modern browsers enforce security policies that applications can opt into via HTTP response headers. Without these headers, browsers fall back to permissive defaults — defaults designed for compatibility with the web of twenty years ago, not for the security requirements of a modern application.

A quick scan with securityheaders.com or Mozilla Observatory will tell you where your current application stands. Most production applications have significant gaps.

```mermaid
flowchart TD
  A["HTTP Response"] --> B["HSTS"]
  A --> C["X-Frame-Options"]
  A --> D["X-Content-Type-Options"]
  A --> E["Referrer-Policy"]
  A --> F["Permissions-Policy"]
  A --> G["COOP and CORP"]
```

## The Essential HTTP Security Headers

### Strict-Transport-Security (HSTS)

HSTS tells browsers to only communicate with your site over HTTPS, even if the user types `http://` in the address bar. Without it, a user on an untrusted network can be the subject of an SSL stripping attack — the attacker downgrades the connection to HTTP before it reaches your server.

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

- `max-age`: how long (in seconds) the browser remembers this policy. Two years (63072000) is standard.
- `includeSubDomains`: applies the policy to all subdomains. Only include this if all your subdomains serve HTTPS correctly.
- `preload`: requests inclusion in browser HSTS preload lists, so the browser enforces HTTPS before ever making a first connection. Submit to hstspreload.org separately.

**In Express (via Helmet):**

```js
const helmet = require('helmet');
app.use(helmet.hsts({
  maxAge: 63072000,
  includeSubDomains: true,
  preload: true
}));
```

**In Nginx:**

```nginx
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
```

### X-Frame-Options

Prevents your page from being embedded in an iframe by other origins. Clickjacking attacks work by overlaying a transparent iframe of your application over a malicious page and tricking users into clicking interface elements they cannot see.

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

Use `DENY` unless your application has a legitimate need to be framed by specific trusted origins, in which case `SAMEORIGIN` allows framing from your own origin only. For fine-grained control, use the CSP `frame-ancestors` directive instead — it supersedes `X-Frame-Options` in browsers that support CSP.

### X-Content-Type-Options

Prevents browsers from MIME-sniffing a response away from the declared content type. Without it, a browser might interpret a text file as executable JavaScript if the content looks like a script.

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

This is a one-liner with no configuration needed. It should be on every application.

### Referrer-Policy

Controls how much information is included in the `Referer` header when users navigate away from your site. The default behavior sends the full URL to external sites, which can leak sensitive path information (user IDs in URLs, tokens in query parameters, internal paths).

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

This sends the full URL for same-origin requests and only the origin (no path) for cross-origin requests. For applications that handle particularly sensitive URLs, `no-referrer` sends nothing at all.

### Permissions-Policy (formerly Feature-Policy)

Restricts which browser features and APIs your application and any embedded third-party content can use.

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

This disables the camera, microphone, geolocation, and payment APIs. Adjust to permit the features your application actually uses. Restricting these prevents a compromised third-party script from silently activating device capabilities.

### Cross-Origin-Opener-Policy (COOP)

Prevents other origins from getting a handle to your window object, which is a prerequisite for certain cross-origin attacks including Spectre side-channel attacks.

```
Cross-Origin-Opener-Policy: same-origin
```

### Cross-Origin-Resource-Policy (CORP)

Prevents other origins from reading resources loaded by your server, protecting against cross-origin information leaks.

```
Cross-Origin-Resource-Policy: same-origin
```

Use `same-site` if your resources need to be loadable by other applications on the same site, or `cross-origin` only for resources explicitly intended to be public (fonts, public images served from a CDN).

### Cross-Origin-Embedder-Policy (COEP)

Required to enable certain powerful browser features (like `SharedArrayBuffer`) and works in conjunction with COOP to provide cross-origin isolation.

```
Cross-Origin-Embedder-Policy: require-corp
```

Note that this header breaks embedding of third-party resources that do not set CORP themselves (such as many ad networks and analytics scripts). Evaluate carefully before enabling.

## Configuring Security Headers in Express

The Helmet middleware for Express sets most of these headers with sensible defaults in a single line:

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

Helmet's defaults are a reasonable starting point. Review and extend them based on your application's specific requirements — particularly CSP and Permissions-Policy, which require application-specific configuration.

## Configuring Security Headers in Nginx

```nginx
server {
    # Existing server config...

    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
    add_header X-Frame-Options "DENY" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
    add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
    add_header Cross-Origin-Opener-Policy "same-origin" always;
    add_header Cross-Origin-Resource-Policy "same-origin" always;
}
```

The `always` flag ensures headers are sent on error responses as well as successful ones.

## Verifying Your Headers

After deployment, verify your headers using:

- **Mozilla Observatory** (observatory.mozilla.org): grades your headers and explains each gap
- **securityheaders.com**: quick header check with letter grade
- **curl**: `curl -I https://yourapp.com` shows response headers directly

Re-run header checks after significant deployments, particularly those that add or change third-party integrations, since new features may require header policy updates.

## What Headers Do Not Cover

Security headers address browser-enforced policies. They do not address server-side vulnerabilities, injection flaws, authentication weaknesses, or misconfigured access control. They are one layer of a defense-in-depth approach, not a substitute for secure application code.

Building a web application and want a team that ships with a complete security baseline? [Start a build](https://clixo.sh/#contact).

---

Clixo · 1141 W Bryn Mawr Ave, Itasca, IL 60143, US · [hello@clixo.sh](mailto:hello@clixo.sh)
[Start a build](https://clixo.sh/#contact) · [All services](https://clixo.sh/services) · [Agent guide (llms.txt)](https://clixo.sh/llms.txt)
