# Next.js security headers and CSP (nonce) — automate defense-in-depth with one middleware

> How to introduce security headers like CSP, HSTS, X-Content-Type-Options, Referrer-Policy, and frame-ancestors all at once with one Next.js App Router middleware. It explains in real code why a hand-written nonce-style CSP breaks and how to automate it, and honestly shows the limit that CSP is a complement to XSS countermeasures, not a substitute for output design.

- Published: 2026-06-28
- Author: 友田 陽大
- Tags: Next.js, セキュリティ, アーキテクチャ設計, TypeScript
- URL: https://tomodahinata.com/en/blog/nextjs-security-headers-csp-nonce-middleware-guide
- Category: Application-layer security
- Pillar guide: https://tomodahinata.com/en/blog/nextjs-supabase-application-security-guide

## Key points

- Security headers are 'horizontal control' — they work uniformly across the app and can be consolidated and automated in one middleware. Each header plugs a different threat (CSP→reduce XSS damage, HSTS→man-in-the-middle, X-Content-Type-Options→MIME sniffing, frame-ancestors→clickjacking, Referrer-Policy→URL leakage, Permissions-Policy→API abuse).
- A CSP without a nonce tends to allow `'unsafe-inline'` and become effectively XSS-defenseless. A safe nonce style requires 'generating it fresh per request,' which is impossible in static config. Generating the nonce in middleware and passing it to both the header and the app is the only correct answer.
- Introduce it in production in phases with Content-Security-Policy-Report-Only. Enforcing it abruptly breaks even legitimate scripts. Observe violation reports, then promote to enforce.
- The honest limit: CSP/headers reduce risk and can be automated, but they're the 'last wall' of XSS and not a substitute for output sanitization = design. The responsibility to safely output tainted data still lies on the code side.

---

Let me state the conclusion first. **Next.js security headers are "horizontal control" that can be consolidated and automated in one `middleware.ts`.** Headers work uniformly on all requests without knowing the app-specific data model at all. That's exactly why the correct answer is to discard the premise "a human writes it correctly every time" and firm it up once in config. The only exception is the **nonce-style CSP,** which alone requires "generating it fresh per request," so it can't be expressed in a static config file — you need to generate it in middleware and pass it to both the header and the app.

But let me draw one honest line here. **CSP is the "last wall" of XSS and isn't a substitute for the safe design of output.** If you pass tainted data straight to `dangerouslySetInnerHTML`, damage occurs even with CSP. This article organizes, as a set, "what you can mechanically plug with headers" and "where headers can't plug and design is needed," based on Next.js App Router real code and primary sources. This is an article that digs into the header area of the "horizontal control vs. vertical risk" map drawn in [Next.js × Supabase application security](/blog/nextjs-supabase-application-security-guide).

---

## 1. Why headers are "automatable horizontal control"

Security measures have two kinds with different natures.

| Kind | Examples | What it depends on | Automation |
|---|---|---|---|
| **Horizontal control** | security headers, CSP, rate limiting, CSRF, input validation | uniform across the app | **can be stood in for uniformly by config** |
| **Vertical risk** | authorization/IDOR, tenant isolation, business logic | "who owns what" | up to detection/warning. The fix is human design |

Security headers are completely the former. `Strict-Transport-Security` and `X-Content-Type-Options: nosniff` don't know your table structure or the user's ownership relationships. They work just by "adding the same one line to all responses." It's an area where **a human shouldn't think per screen, but should consolidate it in one place and let the machine stand guard.**

What becomes this "one place" in Next.js is middleware. `middleware.ts` runs before all requests, so it's ideal as a single checkpoint for adding headers ([Next.js docs](https://nextjs.org/docs)). You can also add static headers with `next.config.js`'s `headers()`, but since the nonce described later changes value per request, it can't be expressed in `next.config.js`. So leaning the header strategy on middleware makes sense.

OWASP's [Application Security Verification Standard (ASVS)](https://owasp.org/www-project-application-security-verification-standard/) also measures security by "can you verify it" rather than "did you put it in." This article too states, for each header, "what it plugs" and "how to confirm it" as a set.

---

## 2. The major security headers, and the threat each plugs

First, let me hand you a map. Headers are not "magic that protects everything with one" but **a set of parts where one plugs one threat.** Confusing them leads to an accident like "I put in CSP but got clickjacked."

| Header | Threat it plugs | Typical value |
|---|---|---|
| `Content-Security-Policy` | XSS (reducing damage), data exfiltration | `default-src 'self'; script-src 'self' 'nonce-…'` |
| `Strict-Transport-Security` | HTTPS downgrade, man-in-the-middle (MITM) | `max-age=63072000; includeSubDomains; preload` |
| `X-Content-Type-Options` | erroneous execution by MIME sniffing | `nosniff` |
| `Referrer-Policy` | external leakage of the URL (tokens, etc.) | `strict-origin-when-cross-origin` |
| `frame-ancestors` (CSP) / `X-Frame-Options` | clickjacking | `frame-ancestors 'none'` / `DENY` |
| `Permissions-Policy` | abuse of camera/mic/geolocation, etc. | `camera=(), microphone=(), geolocation=()` |

Let me pin down just the key points in order, accurately.

**`Strict-Transport-Security` (HSTS)** forces the browser to "come over HTTPS for the next `max-age` seconds for this site." The aim is to subsequently erase the plaintext request of the first access, and attaching `preload` to put it on the browser's pre-read list protects even the first access. But since `preload`, **once put on, is hard to revoke,** do it only when you have a guarantee that you can permanently HTTPS-ify, including subdomains ([MDN Strict-Transport-Security](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Strict-Transport-Security)).

**`X-Content-Type-Options: nosniff`** stops the browser's behavior of "guessing" the response's MIME type and executing. For example, it prevents the accident where the browser arbitrarily interprets a user-uploaded file as `text/html` and executes a script. The value is `nosniff`, the only choice.

**`Referrer-Policy`** controls the granularity of the `Referer` header sent when navigating to another site. If you put a token or session ID in the query string, a loose setting leaks the secret to the external site's access logs. `strict-origin-when-cross-origin` (full path for same-origin, origin only for cross-origin) is a practical default.

For **clickjacking**, CSP's `frame-ancestors` is the modern correct answer. The old `X-Frame-Options` is at most written alongside for backward compatibility, and `frame-ancestors 'none'` (or enumerating the allowed origins) is essentially sufficient. If the two conflict, CSP takes priority.

**`Permissions-Policy`** declares whether to allow browser features like camera, mic, geolocation, and fullscreen to your own site or to embedded iframes. Explicitly closing unused features empty, like `camera=()`, reduces the abuse surface from a possible XSS or a third-party script.

The five so far (HSTS / nosniff / Referrer-Policy / frame-ancestors / Permissions-Policy) have **fixed values.** They don't change per request. The problem is the sixth — only CSP's `script-src` needs "a value that changes per request (a nonce)." Let me dig into it in the next section.

---

## 3. CSP basics — default-src / script-src, and why `'unsafe-inline'` is dangerous

CSP (Content Security Policy) is a header that declares to the browser "**from where this page may load resources, and what it may execute.**" The aim is, even if XSS gets mixed in, not to let the attacker's script execute as "not permitted" ([MDN Content-Security-Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy)).

The directives are "an allowlist per resource kind." Let me pin down just the few most important.

```http
Content-Security-Policy:
  default-src 'self';                          # default: allow same-origin only
  script-src 'self' 'nonce-rAndOm==';          # JS: own origin + only those with this nonce
  style-src 'self';                            # CSS: own origin only
  img-src 'self' data:;                        # images: own origin + data URI
  object-src 'none';                           # forbid <object>/<embed> (close old attack surfaces)
  base-uri 'none';                             # prevent relative-path hijacking via <base> tampering
  frame-ancestors 'none';                      # clickjacking countermeasure
```

`default-src` is the fallback, applied to all resource kinds without an individual specification. `script-src` is the most important directive that holds the execution permission of scripts.

### Why `'unsafe-inline'` is dangerous

This is CSP's core. Many "I just put in CSP for now" configurations are written like this.

```http
# Anti-pattern: this is a setting that "put in CSP" but "barely prevents XSS"
script-src 'self' 'unsafe-inline';
```

`'unsafe-inline'` is the permission "may execute all `<script>…</script>` written directly in the page and inline `onclick=`." But **the essence of XSS is the attacker injecting an inline script into the page.** That is, the moment you allow `'unsafe-inline'`, CSP becomes almost powerless against XSS. It's the state of "I locked the door but put the spare key under the doormat."

Similarly, avoid `'unsafe-eval'` (which allows `eval()` and code generation from strings).

### The correct answer: nonce + strict-dynamic

So how do you allow "only your own legitimate inline scripts"? The answer is a **nonce (number used once).** Generate a random throwaway value per request, (1) declare it in the CSP header as `'nonce-<value>'`, and (2) attach a `nonce="<value>"` attribute to the `<script>` tags you output. **Only scripts where the two match** are executed. Since the attacker can't know in advance the random value the server generated for that request, they can't attach the correct nonce to the injected script.

```http
# Strict version: allow "only legitimate inline" with nonce
script-src 'self' 'nonce-rAndOm==' 'strict-dynamic';
```

`'strict-dynamic'` is the instruction "a script trusted by a nonce chains its trust to scripts it further dynamically loads." With this, you don't need to rely on a host-name allowlist (an enumeration like `https://cdn.example.com`). The allowlist method is broken if there's even one exploitable script on a trusted CDN, so nonce + strict-dynamic is more robust.

**The decisive constraint is here. A nonce has no meaning unless "generated fresh per request."** If all users and all requests use the same fixed value, an attacker can freely inject scripts just by reading that value once, and the meaning of using a nonce vanishes. So a nonce-equipped CSP **fundamentally can't be realized** in a static `next.config.js`'s `headers()` or a build-time-fixed config. You need a place where code runs on each request and makes the value — that is, **middleware.**

---

## 4. Implementation — generate the nonce with one middleware and pass it to the header and the app

This is the implementation core of this article. The difficulty lies in the passing: "make the same generated nonce reach both the response header **and** the rendered `<script>`." In the Next.js App Router, the standard route is to put the nonce on the request header from middleware and read it from `headers()` on the Server Component side.

### 4-1. middleware: nonce generation and header addition

```ts
// middleware.ts — リクエストごとに nonce を発行し、全ヘッダーを1か所で付与する
import { NextResponse, type NextRequest } from "next/server";

export function middleware(request: NextRequest) {
  // リクエストごとに必ず新規生成。crypto は Edge ランタイムでも利用可
  const nonce = crypto.randomUUID().replace(/-/g, "");
  const isDev = process.env.NODE_ENV !== "production";

  const csp = [
    `default-src 'self'`,
    // 開発時は HMR のため eval を許す。本番ビルドでは付けない（下の分岐参照）
    `script-src 'self' 'nonce-${nonce}' 'strict-dynamic'${isDev ? " 'unsafe-eval'" : ""}`,
    // Next.js は styled-jsx 等でインラインstyleを使う。nonce が効かない環境向けの現実解は §6 で後述
    `style-src 'self' 'unsafe-inline'`,
    `img-src 'self' blob: data:`,
    `font-src 'self'`,
    `object-src 'none'`,
    `base-uri 'none'`,
    `form-action 'self'`,
    `frame-ancestors 'none'`,
    `upgrade-insecure-requests`,
  ].join("; ");

  // 1) 後段(Server Component)が同じ nonce を読めるよう、リクエストヘッダーに載せる
  const requestHeaders = new Headers(request.headers);
  requestHeaders.set("x-nonce", nonce);
  requestHeaders.set("Content-Security-Policy", csp);

  const response = NextResponse.next({ request: { headers: requestHeaders } });

  // 2) ブラウザに効かせる本番のヘッダー群（値が固定のものはここで一律付与）
  response.headers.set("Content-Security-Policy", csp);
  response.headers.set("X-Content-Type-Options", "nosniff");
  response.headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
  response.headers.set("X-Frame-Options", "DENY"); // frame-ancestors の後方互換
  response.headers.set(
    "Permissions-Policy",
    "camera=(), microphone=(), geolocation=(), interest-cohort=()",
  );
  // HSTS は HTTPS 環境でのみ意味を持つ。ローカルHTTPで付けると後で苦労するので本番だけ
  if (!isDev) {
    response.headers.set(
      "Strict-Transport-Security",
      "max-age=63072000; includeSubDomains; preload",
    );
  }

  return response;
}

// 静的アセットやプリフェッチには CSP を流さない（不要なオーバーヘッドと誤検知を避ける）
export const config = {
  matcher: [
    {
      source: "/((?!api|_next/static|_next/image|favicon.ico).*)",
      missing: [
        { type: "header", key: "next-router-prefetch" },
        { type: "header", key: "purpose", value: "prefetch" },
      ],
    },
  ],
};
```

Three points. **(1) `crypto.randomUUID()`** is a standard API usable even in the Edge runtime, and gets sufficient randomness with no external dependency (if you want to base64-encode, `btoa`, but removing UUID's hyphens is practically fine too). **(2) Putting the CSP on both the request header and the response header** is because the former is "the passing to the app" and the latter is "the application to the browser" — different roles. **(3) The `matcher`** excludes static assets, suppressing the cost and noise of adding headers.

### 4-2. App side: read the nonce and attach it to `<script>`

Read the value middleware put on `x-nonce` in the Server Component.

```tsx
// app/layout.tsx — middleware が生成した nonce を読み、自前のインラインスクリプトに付与
import { headers } from "next/headers";
import Script from "next/script";

export default async function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  const nonce = (await headers()).get("x-nonce") ?? "";

  return (
    <html lang="ja">
      <body>
        {children}
        {/* 例: アナリティクス等の自前インラインスクリプトには必ず nonce を付ける */}
        <Script
          id="analytics-init"
          nonce={nonce}
          strategy="afterInteractive"
          dangerouslySetInnerHTML={{
            __html: `window.dataLayer = window.dataLayer || [];`,
          }}
        />
      </body>
    </html>
  );
}
```

Calling `headers()` makes that route dynamically rendered. This is an essential trade-off of the nonce method — **since the value changes per request, you can't statically cache that page.** On a page where fully-static delivery is the top priority and there are no own inline scripts at all, there's also the judgment to give up the nonce and choose the hash method (`'sha256-…'`). It's a point to recognize as a design balance.

### 4-3. Operation check

Once implemented, always confirm "is it working." You can't tell from a type check or the appearance.

```bash
# レスポンスヘッダーを直接観測する。nonce がリクエストごとに変わることも確認
curl -sI https://localhost:3000/ | grep -iE "content-security-policy|strict-transport|x-content-type|referrer-policy|permissions-policy"
```

```http
content-security-policy: default-src 'self'; script-src 'self' 'nonce-a1b2c3...' 'strict-dynamic'; ...
strict-transport-security: max-age=63072000; includeSubDomains; preload
x-content-type-options: nosniff
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=(), interest-cohort=()
```

If a violation `Refused to execute inline script because it violates the following Content Security Policy directive` appears in the browser's DevTools Console, that's proof that "a script with a forgotten nonce is being rejected." Using the next section's Report-Only, you can observe this violation without breaking production.

---

## 5. Introduce in phases with Report-Only — don't enforce abruptly

Putting CSP into production in enforce mode from the start is dangerous. Third-party widgets, measurement tags, `next dev`'s behavior, an overlooked own inline script — these get rejected all at once and the screen breaks. **The correct order is "observe → fix → enforce."**

The browser provides the `Content-Security-Policy-Report-Only` header for this. This is a mode that "violates the policy but **doesn't block, only reports the violation.**"

```ts
// 段階導入: まず Report-Only で「何が弾かれるはずか」を観測する
const cspHeaderName = ENFORCE_CSP
  ? "Content-Security-Policy"
  : "Content-Security-Policy-Report-Only";

response.headers.set(cspHeaderName, csp);
// 違反レポートの送信先（report-to/report-uri）。収集は Sentry 等のエンドポイントでも可
response.headers.set(
  "Reporting-Endpoints",
  `csp-endpoint="https://example.com/api/csp-report"`,
);
```

The introduction procedure is as follows.

1. **Deploy to production with Report-Only.** The screen doesn't break at all.
2. **Collect violation reports for a few days to 1–2 weeks.** You learn which scripts/styles/connection destinations get rejected in real traffic.
3. **Add the legitimate ones to the policy.** Attach a nonce to your own scripts, and add only the necessary third parties to the allowance. Repeat until the violations dry up.
4. **Promote to `Content-Security-Policy` (enforce).** Only here does blocking become effective.

Whether you go through this process of "observe real-traffic violations with Report-Only, then promote to enforce" divides the success or failure of CSP introduction. Abruptly enforcing, causing an incident, and removing CSP entirely — this is the most common failure.

---

## 6. Honest pitfalls — Next.js-specific realities

Let me list, without exaggeration, the points you definitely hit in actual operation.

**Handling inline style.** Next.js has scenes that use `styled-jsx` or framework-internal inline styles, and sometimes you can't fully make a nonce work on `style-src`. There are two realistic answers — allow `style-src 'self' 'unsafe-inline'` (the risk of allowing inline style is limited compared to that of script, but it's **not zero,** so recognize that a data-exfiltration-type attack surface remains), or allow them individually with a hash. Rather than aiming for perfection and breaking the screen, keeping `script-src` strict while operating `style-src` realistically lowers the total risk.

**Third-party scripts.** Measurement, chat, and ad tags each bring in the origin they require or the equivalent of `'unsafe-inline'`. Using `'strict-dynamic'` makes many work via the nonce chain, but some tags don't support it. The reverse-direction decision "don't put in a tag that doesn't fit CSP" is also often the correct answer security-wise.

**Dynamization by `headers()`.** As touched on in §4-2, a page that reads the nonce is excluded from ISR/static caching. On a page where full-CDN-caching is business-critical, the hash method, or the judgment to loosen CSP only on that route, is needed.

**The middleware's runtime.** Since middleware runs in the Edge runtime, while standard APIs like `crypto.randomUUID()` are usable, Node.js-specific modules aren't. Be careful not to bring a Node-only library into the nonce generation.

These all show that it's not "put in the header and you're done," but that **adjustment to match the app's composition is needed.** That said, the range of adjustment is closed within the app-wide config, and data-model design judgment is unnecessary. So it's still "horizontal control = an automatable layer."

---

## 7. This is "horizontal control" — automatable with Aegis's init

The implementation so far is, when pushed, **routine work.** Nonce generation, adding the six kinds of headers, phased introduction from Report-Only, setting the matcher — none require app-specific knowledge, and if written correctly once, they're reusable across all projects. Rather than "a human hand-writes the middleware every time," **auto-generating a template and adding only the necessary adjustments** structurally reduces human error (forgetting to remove `'unsafe-inline'`, fixing the nonce, misconfiguring HSTS).

The OSS security toolkit Aegis I publish templates this runtime hardening of horizontal control — **the introduction of headers/CSP, rate limiting, CSRF, and a typed env boundary** — in one command.

```bash
# ランタイム強化の雛形を導入（ヘッダー/CSP・レート制限・CSRF・型付きenv境界）
npx @aegiskit/cli init
```

I don't intend to overclaim. What `init` generates is **a starting-point config,** and the third-party adjustment seen in §6 and the per-page dynamization trade-off need, after all, to be worked out to match your app's composition. Still, starting from a verified template is safer and faster than writing from scratch and stepping on pitfalls. Combined with Aegis's `scan` (visualize current holes with SAST), it becomes the flow "visualize the holes you have now → introduce runtime hardening."

---

## 8. The biggest caveat — CSP is a complement to XSS countermeasures, not a substitute for output design

This is the point I most want to emphasize in this article. **Even if you put in CSP, XSS doesn't "go away."**

CSP is one layer of defense-in-depth, so to speak "the last wall that reduces damage when XSS has gotten mixed in." It raises the probability of hindering the execution of the attacker's script, but **the responsibility to safely output tainted data in the first place** still lies on the app's code side. For example, the following code has a problem remaining even with a strict CSP.

```tsx
// CSP があっても安全設計の代わりにはならない例
// 汚染データをそのまま HTML として注入している
<div dangerouslySetInnerHTML={{ __html: userSuppliedHtml }} />
```

A nonce-style CSP that doesn't allow `'unsafe-inline'` hinders the execution of a `<script>` injected here **in many cases.** But CSP-bypass techniques are continuously researched, and depending on the configuration's holes — `base-uri` deficiency, execution via an allowed old library, abuse of the `'strict-dynamic'` chain — it can be slipped past. Furthermore, attacks not accompanied by script execution (link replacement, form hijacking, phishing by content tampering) aren't stopped by CSP's `script-src`.

**The correct order is this. First make the output safe by design (sanitize/escape tainted data, don't pass it straight to a dangerous sink). On top of that, layer CSP as insurance for "when something still leaks."** Thinking the reverse order — "since there's CSP, sanitization can be sloppy" — is the most dangerous misunderstanding.

How to prevent XSS itself — the safe handling of `dangerouslySetInnerHTML`, DOM XSS, sanitizing tainted data — is detailed separately in [Next.js XSS / DOM XSS prevention](/blog/nextjs-xss-dom-xss-dangerouslysetinnerhtml-prevention-guide). CSP **complements, not replaces,** the countermeasures of that article. This is the very defense-in-depth philosophy that OWASP's [OWASP Top 10](https://owasp.org/www-project-top-ten/) consistently shows.

---

## 9. Other horizontal control alongside headers

Headers/CSP are only part of horizontal control. With the same "firm it up uniformly by config" idea, there are companions that should be leaned on middleware or the boundary. Since they're out of this article's scope, I leave each to a dedicated article.

- **CSRF / Origin verification** — state-changing routes like Server Actions and `POST` routes are protected in two stages by verifying the Origin in addition to `SameSite` cookies. For details, go to [Next.js CSRF/Origin protection](/blog/nextjs-csrf-origin-protection-server-actions-guide).
- **Secret hygiene** — the accident where a secret mixes into the client bundle from misuse of the `NEXT_PUBLIC_` prefix can't be prevented by headers. The design of the env boundary is handled in [Next.js environment-variable / secret-leak prevention](/blog/nextjs-env-secret-leak-prevention-public-vars-guide).
- **Rate limiting / input validation** — these too are horizontal control, summarized as a map in [the application-security overview](/blog/nextjs-supabase-application-security-guide).

What's important is that **these are all "an automatable layer."** They can be stood in for by config and libraries, and you can let CI stand guard. The problem is the "vertical risks that can only be protected by design," like the authorization and tenant isolation beyond that, and that moves to the audit area of the next section.

---

## 10. Pre-production checklist

Whether outsourced or AI-made, confirm at least this before going to production.

- [ ] You **don't put `'unsafe-inline'` (script) in CSP.** If you do, use a nonce/hash
- [ ] **The nonce changes per request** (hit `curl -I` twice and the value differs)
- [ ] You **attach a nonce** to all your own inline scripts / `<Script>`
- [ ] You **observed real-traffic violations with Report-Only** before promoting to enforce
- [ ] You attach `Strict-Transport-Security` **only in production (HTTPS)** (understanding `preload`'s irreversibility)
- [ ] You attach `X-Content-Type-Options: nosniff` / `Referrer-Policy` / `frame-ancestors` (+ `X-Frame-Options`) / `Permissions-Policy`
- [ ] You exclude static assets with the `matcher` and avoid unnecessary header addition
- [ ] You **don't make CSP "a substitute for sanitization."** The output of tainted data is made safe on the design side
- [ ] The middleware is **Edge-runtime compatible** (doesn't depend on Node-only APIs)

What's effective from the client's viewpoint is the three questions **"Is `'unsafe-inline'` in CSP?" "Does the nonce change per request?" "Did you introduce it in phases with Report-Only?"** A good developer can answer immediately.

---

## 11. How far to automate, and where audit begins

Finally, let me draw the line honestly.

**Security headers and CSP can be mechanically firmed up for the majority as horizontal control.** Consolidate them in one middleware, start from a template, and introduce in phases with Report-Only — if you keep this form, a human doesn't need to think from zero every time. First putting in a template with [Aegis](/aegis) (free OSS, runtime hardening with `npx @aegiskit/cli init`, grasp the current state with `scan`) is the most cost-effective first step.

On the other hand, **even if CSP reduces XSS damage, it doesn't work at all on the "vertical risks that appear as legitimate requests," like authorization, tenant isolation, and business logic.** These can't be structurally prevented by headers or a WAF, and can only be designed/verified by a human who understands your data model. Thinking "I put in headers, so it's safe" here is the most dangerous complacency. Headers are insurance at the entrance and don't prove the correctness of authorization.

That's exactly why a line is needed. How far to firm up yourself, and where an expert's review is needed — if you need that judgment, or a review of an existing app including authorization, RLS, and output design, I take it on with a [security audit](/aegis/audit). I myself, in the [serverless-payment-platform project](/case-studies/payment-platform-reliability), designed and verified the hardening of the application layer including the payment-reliability layer in actual operation.

---

## Frequently asked questions (FAQ)

**Q. Isn't `next.config.js`'s `headers()` fine?**
A. For fixed-value headers (HSTS, nosniff, Referrer-Policy, etc.), you can add them with `headers()`. But **a nonce changes per request, so it can't be expressed in `next.config.js`.** If you operate CSP in the nonce style, middleware is mandatory. Consolidating even the fixed headers in middleware makes the checkpoint one and easier to manage.

**Q. Which should I use, nonce or hash?**
A. If your own inline scripts change dynamically, or there are multiple, **nonce** is easier to handle. If there are no inline scripts at all, or they're fixed, and you want to cache the page fully statically, **hash (`'sha256-…'`)** suits. With hash you don't need to call `headers()` and can maintain static delivery. The majority of apps are fine with a nonce, but the use distinction of hash only on pages where full-CDN-caching is the top priority is also effective.

**Q. If I put in CSP, can I prevent XSS?**
A. You can't. CSP is **the last wall that reduces damage** when XSS gets mixed in, and isn't a substitute for the safe design of output (sanitization/escaping). CSP-bypass techniques also exist. First make the output safe by design, and on top of that layer CSP as insurance — this order is mandatory. For details, see the [XSS-countermeasure article](/blog/nextjs-xss-dom-xss-dangerouslysetinnerhtml-prevention-guide).

**Q. Can I keep operating in Report-Only?**
A. No good. Report-Only **blocks nothing.** It's a mode for the transition period of observing violations, and stopping here results in the state "it looks like there's CSP, but it actually protects nothing." Once the violations dry up, always promote to `Content-Security-Policy` (enforce).

**Q. Should I go this far even for personal development or small scale?**
A. Headers are horizontal control, with extremely low introduction cost (one middleware) yet a broad attack surface they can plug. They're one of the highest-cost-effectiveness measures. At minimum, the four — nosniff, Referrer-Policy, frame-ancestors, HSTS — and a CSP that doesn't use `'unsafe-inline'` are worth putting in regardless of scale.

---

## Summary — consolidate in one, layer as insurance

Let me organize the key points.

- Security headers are **"horizontal control" that works uniformly across the app.** Consolidating and automating them in one `middleware.ts` is the correct answer.
- Each header plugs **a different threat one by one** (CSP→reduce XSS damage, HSTS→MITM, nosniff→MIME sniffing, frame-ancestors→clickjacking, Referrer-Policy→URL leakage, Permissions-Policy→feature abuse). One can't protect everything.
- Only a nonce-style CSP **requires per-request generation** and can't be realized in static config. Generate it in middleware and pass it to both the header and the app.
- Introduce it in production by **observe with Report-Only → fix → promote to enforce.** Enforcing abruptly breaks it.
- These can be templated (`npx @aegiskit/cli init`) and are an automatable layer. **But CSP is the last wall of XSS and isn't a substitute for output sanitization = design.**
- The authorization and tenant isolation that headers can't plug are vertical risks, the area of design and audit.

Building fast with AI is itself correct. **Firming up what you built fast, safely, without leaks** — if you need the automation of that horizontal control, or a review of an existing app's headers/CSP and output design, please feel free to consult me.

---

## References

- [MDN — Content-Security-Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy)
- [MDN — Strict-Transport-Security](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Strict-Transport-Security)
- [Next.js Docs](https://nextjs.org/docs)
- [OWASP Top 10](https://owasp.org/www-project-top-ten/)
- [OWASP Application Security Verification Standard (ASVS)](https://owasp.org/www-project-application-security-verification-standard/)
