> How the Nuxt Security module behaves in practice: safe defaults, strict mode trade-offs, nonce CSP in SSR and SSG, route rules and limits.
>
> Web page: https://balazscsorba.com/blog/nuxt-security-module · Language: English · Also available in: [Deutsch](https://balazscsorba.com/de/blog/nuxt-security-module.md) · [Magyar](https://balazscsorba.com/hu/blog/nuxt-security-module.md)
> Author: Balázs Csorba · Published: 2026-10-02 · Keywords: nuxt security module, nuxt content security policy nonce, nuxt security headers, nitro route rules security, nuxt rate limiter, nuxt cors server routes, csp report-uri nuxt, nuxt csrf protection, secure nuxt app checklist, nuxt csp strict-dynamic breakage

[Blog](https://balazscsorba.com/blog)/Web engineering

# Locking down a Nuxt app: CSP, headers and rate limits with Nuxt Security

How the Nuxt Security module behaves in practice: safe defaults, strict mode trade-offs, nonce CSP in SSR and SSG, route rules and limits.

[Balázs Csorba](https://balazscsorba.com/about)·October 2, 2026·11 min read

-   Nuxt
-   Content Security Policy
-   HTTP security headers
-   Rate limiting
-   Nitro

![A shield with three locks guards a browser window.](https://balazscsorba.com/images/blog/nuxt-security-module/cover.webp?v=e2f8a194e0)

## Key takeaways

-   Nuxt Security is a useful default layer for headers and request limits, but its documented feature set does not include authentication, authorization, SSRF protection or a web application firewall.
-   The defaults keep a typical app working. Strict mode is a different policy, so fix the breakages in report-only mode before you enforce it.
-   Nonce CSP needs server rendering. Static builds use build-time hashes in a meta tag, so test each kind of page separately.
-   The built-in rate limiter is an application-layer speed bump. Brute-force protection belongs at the infrastructure layer.
-   Roll out in report-only mode, assert the real headers in end-to-end tests, and write exceptions per route rather than loosening the whole site.

On this page

1.  [What does Nuxt Security cover, and what does it leave out?](https://balazscsorba.com/#what-nuxt-security-covers)
2.  [What do the defaults guarantee, and what does strict mode change?](https://balazscsorba.com/#defaults-versus-strict-mode)
3.  [How does nonce CSP work with SSR, SSG and hybrid apps?](https://balazscsorba.com/#nonce-csp-ssr-ssg-hybrid)
4.  [How do you harden individual routes?](https://balazscsorba.com/#per-route-hardening)
5.  [What do the rate and size limits actually protect?](https://balazscsorba.com/#rate-limits-request-size)
6.  [How do CORS and CSRF behave for API routes?](https://balazscsorba.com/#cors-csrf-api-routes)
7.  [How do you roll out and test the header layer?](https://balazscsorba.com/#observing-and-testing)
8.  [When is hand-rolled Nitro middleware the better choice?](https://balazscsorba.com/#when-to-hand-roll)
9.  [What does a production checklist look like?](https://balazscsorba.com/#production-checklist)
10.  [Sources](https://balazscsorba.com/#sources)

Listen to this article

0:000:00

**Nuxt Security** is a module that adds a hardening layer to a Nuxt application. It sets security response headers, including a Content Security Policy (CSP), adds request size and rate limiters, validates request input for cross-site scripting, handles CORS and removes a few framework leaks. As of October 2026 the latest release is 2.6.0, published on 12 May 2026, and the module works with Nuxt 3.x and newer, including 4.x. The defaults are designed to keep a typical app safe without breaking it. That is useful, but it is not the whole story: some protections depend on whether a page is rendered on the server or generated as static files, some break third-party widgets, and the rate limiter is explicitly not meant for complex production applications.

This article treats the module as an operations layer rather than a feature list. It explains what the defaults guarantee and what strict mode changes, how nonce-based CSP differs between server rendering and static generation, how to override rules per route, why the built-in rate limiter is a speed bump rather than protection against denial of service, and when hand-rolled Nitro middleware is the better choice. Every number comes from the module’s documentation or source as checked in October 2026.

## What does Nuxt Security cover, and what does it leave out?

Nuxt Security covers the HTTP layer: security response headers including CSP for static sites, request size and rate limiters, XSS validation on GET and POST requests, CORS, hiding the X-Powered-By header, and removing console logger utilities. Allowed HTTP methods, basic auth and CSRF protection are optional. The [module README](https://github.com/Baroshem/nuxt-security/blob/main/README.md) lists this as the feature set and states that it works with Nuxt 3.x and newer, including 4.x.

The gaps matter as much as the list. The documented feature set contains no SSRF protection, no authentication or authorization, and no web application firewall. If your app fetches URLs that users supply, or serves private data behind a login, the module does not do that work for you. Treat the XSS validator as one filter on incoming request data, built on js-xss. Escaping output in your components remains your responsibility.

## What do the defaults guarantee, and what does strict mode change?

The defaults give a policy that works with a typical app. Scripts may come from your own origin and from https: sources, styles may use inline CSS, and the script policy names a nonce together with `'strict-dynamic'`. Browsers that understand nonces ignore the unsafe-inline fallback when a nonce is present. The other defaults are `nonce: true`, `sri: true`, a Cross-Origin-Embedder-Policy of `credentialless` in production, HSTS with a max-age of 15,552,000 seconds and includeSubdomains, `no-referrer` as the referrer policy, and `SAMEORIGIN` for X-Frame-Options. Rate limits, request size limits and CORS are on, while basic auth and CSRF are off.

Strict mode is a different policy, not a stricter set of the same values. `strict: true` sets `default-src` to `'none'`, removes `unsafe-inline` from scripts, allows styles only with the nonce, limits frames and workers to `'self'`, switches COEP to `require-corp`, raises HSTS to one year with preload, changes X-Frame-Options to `DENY` and denies every Permissions Policy feature by default. The docs describe a gradual path: start with the defaults, fix what breaks, and then migrate to strict.

Feature

Default

Per route

Caveat you hit first

CSP for SSR

nonce plus strict-dynamic

yes, under security

nonce is ignored in static builds

CSP for SSG

build-time hashes in a meta tag

not documented per route

inline code must be hashed

Rate limiter

150 requests per 300,000 ms per IP

yes, under security

LRU state lives in each process

Request size

2,000,000 bytes, 8,000,000 for uploads

yes, under security

413 on oversized requests

XSS validator

on for GET and POST

yes, under security

400 when unsafe code appears

CORS

origin is the server URL

yes, under security

useRegExp treats origins as patterns

CSRF

off

exempt with csurf: false

opt-in through nuxt-csurf

Subresource integrity

on, hashes at build time

not documented per route

host rewrites break the hashes

COEP

credentialless in production

yes, under security

popup sign-in flows need relaxing

**Strict mode is a migration, not a toggle**

Switching a live app to strict will break third-party widgets, inline handlers and image error scripts. Run the policy in report-only mode first, fix each violation, and only then enforce.

![Three rungs of script policy Three boxes from left to right: allowing inline code, requiring a nonce or hash, and adding strict-dynamic. Each rung breaks more code and gives more control.](https://balazscsorba.com/images/blog/nuxt-security-module/diagram-10.webp?v=bccdabb72f)

Three rungs of script policy. Level 1 allows inline code and breaks the least. Level 2 requires a nonce or hash, which cancels unsafe-inline, so inline event handlers and inline styles start to fail. Level 3 adds strict-dynamic, so only nonced scripts run and the scripts they load are trusted, which breaks code that expects any script tag to work.

## How does nonce CSP work with SSR, SSG and hybrid apps?

With server rendering, the module generates a nonce for each response, puts it into the CSP header, and adds it to the script, link and style elements in the rendered HTML. With static generation that cannot work, because one file serves every visitor. The module computes hashes at build time and delivers the policy in a `<meta http-equiv>` tag. The documentation is explicit that the `nonce` option and the `'nonce-{{nonce}}'` placeholders are ignored when you build for SSG with `nuxi generate`.

![Request path through Nuxt Security Six boxes from left to right: route rules, middleware, the server-side render, nonce insertion, HTML recombination and the headers set on the response.](https://balazscsorba.com/images/blog/nuxt-security-module/diagram-13.webp?v=a4d6d94dc6)

On the server path, the request passes route rules and middleware, the page renders, the nonce is inserted into script, link and style tags, the HTML is rebuilt, and the headers are set. Static builds skip the server and ship a hashed policy in a meta tag.

The rewriting happens inside the render pipeline, which is why application code rarely needs to know about nonces. The module parses inline elements as well as external resources. When your own code needs a nonce, for example for an inline script it creates, `useNonce()` returns the value from the server context:

```
<script setup lang="ts">
const nonce = useNonce() // set during SSR; empty on a static build
useHead({
  script: [{ children: 'window.dataLayer = window.dataLayer || []', nonce }],
})
</script>
```

A static build has no request, so the value is empty there, and inline code that must run on a prerendered page needs a hash instead. The hybrid case causes most of the confusion. A hybrid app mixes server-rendered and prerendered routes, so the two kinds of response can carry different policies, and a header check on one path says nothing about the other. Check both kinds of page.

Three breakages recur. Third-party code that creates script tags at runtime tends to produce elements the policy does not know about. The module’s FAQ says that the inline error-handling scripts of `<NuxtImg>` and `<NuxtPicture>` conflict with a nonce-only policy in SSR. And hosting providers that post-process your bundle after the build invalidate the subresource integrity hashes, which is covered in the next sections.

## How do you harden individual routes?

Override the module per path under `routeRules`, using the `security` field. Every middleware and header can be set there, so an expensive API path can have a tighter rate limit than your marketing pages without a separate server.

```
// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['nuxt-security'],
  security: {
    strict: true,
    contentSecurityPolicyReportOnly: true,
    rateLimiter: { tokensPerInterval: 150, interval: 300000 },
  },
  routeRules: {
    // illustrative values: a tighter limit for one expensive endpoint
    '/api/generate/**': { security: { rateLimiter: { tokensPerInterval: 30, interval: 60000 } } },
  },
})
```

Two details trip people up. First, security headers belong only in the `security` field. The docs advise against setting them through native `routeRules.headers`, and when both define the same header, the security field wins. Second, some values should not live in the repository at all. The `nuxt-security:routeRules` Nitro hook runs when the server restarts and can change the whole rule set, which suits a list of allowed `connect-src` origins fetched from a secret manager. Its build-time sibling, `nuxt-security:prerenderedHeaders`, covers static hosting.

This is where an app with LLM features grows its policy. Streaming responses need their origin in `connect-src`, and each vendor widget needs an entry in `script-src`. The per-route `security` field keeps each exception next to the route that needs it, instead of loosening the whole site. The same streaming setup is described in [the Nuxt LLM streaming article](https://balazscsorba.com/blog/nuxt-llm-features-ai-sdk-streaming), and it is exactly the kind of endpoint where a broad exception tends to creep in.

## What do the rate and size limits actually protect?

They slow down one client that sends too many requests, or oversized bodies, to one running instance. The default allows 150 requests per IP within a 300,000 ms (five-minute) window, tracked in an LRU cache, and answers over-limit requests with `429 Too Many Requests`. The request size limiter enforces 2,000,000 bytes on GET, POST and DELETE requests and answers larger ones with `413 Payload Too Large`; upload routes get 8,000,000 bytes.

The module’s documentation states the limit of this approach. The built-in rate limiter “is not suitable for complex production applications”, it is meant to help simpler applications with brute forcing, and for production you “should use solutions that work on the infrastructure layer, not the application layer”. I read that as the working rule: the built-in limiter is a speed bump, not denial-of-service protection. A CDN, a load balancer or a web application firewall sees the traffic before the Node process does, and can refuse it at a cost your application never pays. This limiter is the thin application-layer part of the defense that [the denial-of-wallet article](https://balazscsorba.com/blog/llm-endpoint-abuse-denial-of-wallet) describes for one expensive endpoint.

Two practical points follow. The LRU default lives inside one process, so with several instances, or serverless functions that do not share memory, each instance counts separately and the effective limit multiplies. That is my reading of the design, not a quoted statement, and a two-instance test will confirm it. The unstorage driver can move the counters to shared storage, such as the `vercelKv` driver the docs show. Second, behind a proxy the client address is the proxy address unless you set `ipHeader`, for example to `cf-connecting-ip`. Set it only when the app can be reached solely through that proxy, because otherwise a client can send the header itself.

## How do CORS and CSRF behave for API routes?

CORS is enabled globally, and by default it allows your own server URL as the origin. CSRF protection is off until you turn it on, and it comes from an integration with nuxt-csurf. The CORS handler is built on H3’s built-in CORS functionality. You can set `methods`, `allowHeaders`, `exposeHeaders`, `credentials` and `maxAge`, and the preflight status defaults to 204. With `useRegExp` enabled, each origin string is compiled into a case-insensitive regular expression. That is convenient for preview deployments and dangerous when a pattern is too loose, so keep the list explicit wherever you can. The [CORS handler docs](https://raw.githubusercontent.com/Baroshem/nuxt-security/main/docs/content/3.middleware/4.cors-handler.md) show the full option list.

CSRF is opt-in. Set `csrf: true`, exempt individual routes with the separate `csurf: false` route rule, and on the client use the auto-imported `useCsrfFetch` and `useCsrf()` composables. The docs also show sending the token in a `csrf-token` header for a tRPC client. Basic auth is opt-in too. The module moves its credentials into `runtimeConfig.private.basicAuth`, which keeps them out of the client bundle. That is the right place for them, but it is still one shared password, so I would not use it as the login for a public product.

## How do you roll out and test the header layer?

Roll out a policy in report-only mode, read the reports, and then enforce it. Set `contentSecurityPolicyReportOnly: true` and the response carries `Content-Security-Policy-Report-Only` instead of the enforcing header. The CSP options accept `report-uri` and `report-to`, both globally and per route. Report-only support arrived in release 2.6.0, which also changed how CSP hashes are computed, from the final HTML in the render hook. Read those release notes before you upgrade a static site.

For a one-off grade of a deployed site, [MDN’s HTTP Observatory](https://developer.mozilla.org/en-US/observatory) checks the headers quickly. It grades what it sees but cannot tell you whether your pages still work, so it complements browser testing rather than replacing it. The check that catches regressions is an assertion in an end-to-end test. [Playwright’s Response class](https://playwright.dev/docs/api/class-response) exposes the response headers, so a test can fail when a deploy drops the policy:

```
import { test, expect } from '@playwright/test'

test('the home page sends the security headers', async ({ page }) => {
  const response = await page.goto('/')
  const headers = response!.headers()
  expect(headers['content-security-policy'] ?? headers['content-security-policy-report-only']).toContain("script-src 'self'")
  expect(headers['strict-transport-security']).toContain('max-age=')
})
```

Run the check against one server-rendered page and one prerendered page, because they produce different policies. The same Playwright setup I already use through Playwright MCP can run it on every deploy, so a header regression shows up in the same place as a broken page. The same habit of measuring before deciding is what I apply in [the Core Web Vitals article](https://balazscsorba.com/blog/nuxt-core-web-vitals-performance), only here the metric is a response header.

## When is hand-rolled Nitro middleware the better choice?

Use the module when you want nonce rewriting, hashing and header sets maintained for you. Hand-roll `server/middleware` when you need exact control over every response and no convention to work around. The module gives you things that are tedious to build well. Nonce insertion happens inside the render pipeline, so it reaches elements the framework emits. Hashes for static builds come from the final HTML. Header sets differ by resource type, with a full policy for HTML and a smaller set for other assets, and per-route rules merge with your global configuration. Those are the parts I would not rewrite.

Hand-rolled middleware wins elsewhere. A few headers and a small allowlist in `server/middleware` are easy to read, need no module conventions and add no dependency. If you only need HSTS, a referrer policy and frame options, write those three headers yourself.

Re-implementing nonce CSP by hand is the worst starting point. The nonce has to reach every script, link and style the render emits, the same value must match the header, and static builds need a different mechanism altogether. Mistakes do not show up in a unit test. They show up as a blank page in one browser. My recommendation is to take the module for the policy and write any exception yourself.

## What does a production checklist look like?

Before enforcing a policy on a production app, I would work through these items in order:

1.  Run the defaults on every page type and fix what the browser console reports as a violation.
2.  Enable `contentSecurityPolicyReportOnly` with a `report-uri` or `report-to` endpoint, and read the reports for a full release cycle.
3.  Confirm which routes are server-rendered and which are prerendered, and check the built HTML for the nonce or the hashed meta tag.
4.  Load third-party scripts with `useScript` (Nuxt 3.11 or newer), not by injecting tags by hand.
5.  List external image hosts in `img-src` and every API origin in `connect-src`, including LLM endpoints.
6.  Check that your host does not rewrite bundles after the build, or the integrity hashes will fail.
7.  Put brute-force and volume protection at the edge, and treat the built-in limiter as a second layer.
8.  Assert the headers in an end-to-end test on one server-rendered and one prerendered page.

The module is a good default layer, and the work that remains is the same as before: knowing which scripts your pages load and which origins your app calls. If you want to see the kind of Nuxt work behind this article, my [Vue and Nuxt projects](https://balazscsorba.com/expertise/vue-nuxt-developer) are a good starting point.

## Sources

1.  [Nuxt Security README (main branch)](https://github.com/Baroshem/nuxt-security/blob/main/README.md)
2.  [Nuxt Security default configuration (src/defaultConfig.ts)](https://raw.githubusercontent.com/Baroshem/nuxt-security/main/src/defaultConfig.ts)
3.  [Nuxt Security docs: Content Security Policy](https://raw.githubusercontent.com/Baroshem/nuxt-security/main/docs/content/2.headers/1.csp.md)
4.  [Nuxt Security docs: rate limiter](https://raw.githubusercontent.com/Baroshem/nuxt-security/main/docs/content/3.middleware/1.rate-limiter.md)
5.  [Nuxt Security docs: CORS handler](https://raw.githubusercontent.com/Baroshem/nuxt-security/main/docs/content/3.middleware/4.cors-handler.md)
6.  [Nuxt Security docs: CSRF](https://raw.githubusercontent.com/Baroshem/nuxt-security/main/docs/content/3.middleware/7.csrf.md)
7.  [Nuxt Security docs: hooks](https://raw.githubusercontent.com/Baroshem/nuxt-security/main/docs/content/5.advanced/4.hooks.md)
8.  [Nuxt Security docs: security headers](https://raw.githubusercontent.com/Baroshem/nuxt-security/main/docs/content/5.advanced/5.security-headers.md)
9.  [Nuxt Security docs: improving security](https://raw.githubusercontent.com/Baroshem/nuxt-security/main/docs/content/5.advanced/7.improve-security.md)
10.  [Nuxt Security release v2.6.0 (12 May 2026)](https://github.com/Baroshem/nuxt-security/releases/tag/v2.6.0)
11.  [MDN: HTTP Observatory](https://developer.mozilla.org/en-US/observatory)
12.  [Playwright: Response class](https://playwright.dev/docs/api/class-response)

## Frequently asked questions

Does Nuxt Security protect against SSRF?

Its documented feature list does not include SSRF protection, and the module does not add authentication or authorization either. Validate outbound URLs in your own code, and re-check the release notes before you rely on a claim about a specific version.

Is the default Nuxt Security CSP enough for production?

The defaults are designed not to break a typical app, but they are a starting point. They keep unsafe-inline as a fallback and allow https: sources for scripts. Read the policy in your built output, fix what breaks, and then move to strict mode or a tighter custom policy.

Why does nonce CSP not work on a static site?

A nonce is generated per response, and a static file is the same for every visitor. Nuxt Security ignores the nonce option during nuxi generate and uses build-time hashes delivered in a meta tag instead.

Does the rate limiter stop denial-of-service attacks?

No. The documentation says it is meant for simpler applications and recommends infrastructure-layer solutions for brute-force protection. Put a CDN, load balancer or web application firewall in front for traffic volume, and keep the application limiter as a second layer.

How do I check that the headers survive a deploy?

Add an end-to-end test that reads response headers on one server-rendered page and one prerendered page, and fails when the Content-Security-Policy or Strict-Transport-Security header is missing.

Written by Balázs Csorba

Senior fullstack & AI engineer in Styria, Austria – 10+ years of Vue, Nuxt, Node.js and PHP, now building tooling for AI agents.

[Vue.js & Nuxt development →](https://balazscsorba.com/expertise/vue-nuxt-developer)[About me →](https://balazscsorba.com/about)

## More articles

-   [Schema.org structured data for B2B shops: prices behind logins, units and variants](https://balazscsorba.com/blog/schema-org-structured-data-b2b-shop)
-   [An AI assistant for B2B shop support: tools, permissions and the human handoff](https://balazscsorba.com/blog/ai-assistant-b2b-shop-customer-support)
-   [Streaming AI chat in Vue and Nuxt: accessible by design](https://balazscsorba.com/blog/vue-ai-chat-ui-streaming-accessibility)
-   [An LLM translation pipeline for i18n: glossary, placeholders and review](https://balazscsorba.com/blog/llm-translation-pipeline-i18n)

## Sounds like what you need?

Tell me about your project or role – I’d love to hear from you.

[Get in touch](mailto:contact@balazscsorba.com) [Connect on LinkedIn](https://www.linkedin.com/in/balazs-csorba)
