diff --git a/docs/content/1.documentation/1.getting-started/1.setup.md b/docs/content/1.documentation/1.getting-started/1.setup.md
index f35961b5..1189753c 100644
--- a/docs/content/1.documentation/1.getting-started/1.setup.md
+++ b/docs/content/1.documentation/1.getting-started/1.setup.md
@@ -24,18 +24,3 @@ security: {
```
You can find more about configuring `nuxt-security` [here](/documentation/getting-started/configuration).
-
-## Using with Nuxt DevTools
-
-In order to make this module work with Nuxt DevTools add following configuration to your projects:
-
-```js{}[nuxt.config.ts]
-export default defineNuxtConfig({
- modules: ['nuxt-security', '@nuxt/devtools'],
- security: {
- headers: {
- crossOriginEmbedderPolicy: process.env.NODE_ENV === 'development' ? 'unsafe-none' : 'require-corp',
- },
- },
-});
-```
diff --git a/docs/content/1.documentation/2.headers/1.csp.md b/docs/content/1.documentation/2.headers/1.csp.md
index 0379afa5..1b63ef22 100644
--- a/docs/content/1.documentation/2.headers/1.csp.md
+++ b/docs/content/1.documentation/2.headers/1.csp.md
@@ -215,15 +215,6 @@ export default defineNuxtConfig({
- `"'nonce-{{nonce}}'"` placeholder: Include this value in any individual policy that you want to be governed by nonce.
-::alert{type="warning"}
-Our default recommendation is to avoid using the `"'nonce-{{nonce}}'"` placeholder on `style-src` policy.
-
-⚠ This is because Nuxt's mechanism for Client-Side hydration of styles could be blocked by CSP in that case.
-
-For further discussion and alternatives, please refer to our [Advanced Section on Strict CSP](/documentation/advanced/strict-csp).
-::
-
-
_Note: Nonce only works for SSR. The `nonce` option and the `"'nonce-{{nonce}}'"` placeholders are ignored when you build your app for SSG via `nuxi generate`._
@@ -304,28 +295,6 @@ Please see below our section on [Integrity Hashes For SSG](#integrity-hashes-for
_Note: Hashes only work for SSG. The `ssg` options are ignored when you build your app for SSR via `nuxi build`._
-
-## Hot reload during development
-
-If you have enabled `nonce-{{nonce}}` on `style-src`, you will need to disable it in order to allow hot reloading during development.
-
-```ts
-export default defineNuxtConfig({
- security: {
- nonce: true,
- headers: {
- contentSecurityPolicy: {
- 'style-src': process.env.NODE_ENV === 'development' ?
- ["'self'", "'unsafe-inline'"] :
- ["'self'", "'unsafe-inline'", "nonce-{{nonce}}"]
- }
- }
- }
-})
-```
-
-Note that this is not necessary if you use our default configuration settings.
-
## Per-route configuration
All Content Security Policy options can be defined on a per-route level.
diff --git a/docs/content/1.documentation/3.middleware/4.cors-handler.md b/docs/content/1.documentation/3.middleware/4.cors-handler.md
index f4a53280..f34155a9 100644
--- a/docs/content/1.documentation/3.middleware/4.cors-handler.md
+++ b/docs/content/1.documentation/3.middleware/4.cors-handler.md
@@ -48,8 +48,9 @@ You can also disable the middleware globally or per route by setting `corsHandle
CORS handler accepts following configuration options:
```ts
-interface H3CorsOptions {
- origin?: '*' | 'null' | (string | RegExp)[] | ((origin: string) => boolean);
+interface CorsOptions = {
+ origin?: '*' | string | string[];
+ useRegExp?: boolean;
methods?: '*' | HTTPMethod[];
allowHeaders?: '*' | string[];
exposeHeaders?: '*' | string[];
@@ -65,7 +66,16 @@ interface H3CorsOptions {
- Default: `${serverUrl}`
-The Access-Control-Allow-Origin response header indicates whether the response can be shared with requesting code from the given origin.
+The Access-Control-Allow-Origin response header indicates whether the response can be shared with requesting code from the given origin. Use `'*'` to allow all origins. You can pass a single origin, or a list of origins.
+
+### `useRegExp`
+
+Set to `true` to parse all origin values into a regular expression using `new RegExp(origin, 'i')`.
+You cannot use RegExp instances directly as origin values, because the nuxt config needs to be serializable.
+When using regular expressions, make sure to escape dots in origins correctly. Otherwise a dot will match every character.
+
+The following matches `https://1.foo.example.com`, `https://a.b.c.foo.example.com`, but not `https://foo.example.com`.
+`'(.*)\\.foo.example\\.com'`
### `methods`
diff --git a/docs/content/1.documentation/5.advanced/2.faq.md b/docs/content/1.documentation/5.advanced/2.faq.md
index 9bc1a15d..2f5eb01b 100644
--- a/docs/content/1.documentation/5.advanced/2.faq.md
+++ b/docs/content/1.documentation/5.advanced/2.faq.md
@@ -245,43 +245,14 @@ Next, you need to configure your img tag to include the `crossorigin` attribute:
ℹ Read more about it [here](https://github.com/Baroshem/nuxt-security/issues/138#issuecomment-1497883915).
::
-## Using nonce with CSP for Nuxt Image
+## Nuxt Image
-Having securely configured images is crucial for modern web applications. Check out how to do it below:
-
-```ts
-// nuxt.config.ts
-
-security: {
- nonce: true,
- headers: {
- contentSecurityPolicy: {
- 'img-src': ["'self'", 'data:', 'https:'],
- 'script-src': [
- "'self'", // backwards compatibility for older browsers that don't support strict-dynamic
- "'nonce-{{nonce}}'",
- "'strict-dynamic'"
- ],
- 'script-src-attr': ["'self'"]
- }
- }
-}
-```
-
-And then configure `NuxtImg` like following:
-
-```vue
-
-
-
-
-
-```
+When using `` or ``, an inline script will be used for error handling during SSR.
+This will lead to CSP issues if `unsafe-inline` is not allowed and the image fails to load.
+Using nonces for inline event handlers is not supported, so currently there is no workaround.
::alert{type="info"}
-ℹ Read more about it [here](https://github.com/Baroshem/nuxt-security/issues/218#issuecomment-1736940913).
+ℹ Read more about it [here](https://github.com/nuxt/image/issues/1011#issuecomment-2242761992).
::
## Issue on Firefox when using IFrame
diff --git a/docs/content/1.documentation/5.advanced/3.strict-csp.md b/docs/content/1.documentation/5.advanced/3.strict-csp.md
index 86f735ac..2693544d 100644
--- a/docs/content/1.documentation/5.advanced/3.strict-csp.md
+++ b/docs/content/1.documentation/5.advanced/3.strict-csp.md
@@ -312,7 +312,7 @@ export defaultNuxtConfig({
### The `useScript` composable
-Starting from Nuxt 3.11, it is possible to insert any external script in one single line with the new `useScript` composable.
+The Nuxt Scripts module allows you to insert any external script in one single line with its `useScript` composable.
```ts
useScript('https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js')
@@ -324,24 +324,24 @@ The `useScript` method has several key features:
- It does not insert inline event handlers, therefore CSP will never block the script from executing after load
- It is designed to load and execute asynchronously, which means you don't have to write code to check whether the script has finished loading before using it
-For all of these reasons, we strongly recommend `useScript` as the best way to load your external scripts in a CSP-compatible way.
+In addition, Nuxt Scripts provide easy integration of `useScript` into any Nuxt application:
+- A number of standard scripts are already pre-packaged
+- You can load your scripts globally in `nuxt.config.ts`
+- `useScript` is auto-imported
-The `unjs/unhead` repo has a [detailed section here](https://unhead.unjs.io/usage/composables/use-script) on how to use `useScript`.
+For all of these reasons, we strongly recommend using the Nuxt Scripts module as the best way to load your external scripts in a CSP-compatible way.
-Check out their examples and find out how easy it is to include Google Analytics in your application:
+Check out their examples on [@nuxt/scripts](https://scripts.nuxt.com) and find out how easy it is to include Google Analytics in your application:
```ts
-import { useScript } from 'unhead'
-
-const { gtag } = useScript({
- src: 'https://www.google-analytics.com/analytics.js',
-}, {
+const { gtag } = useScript('https://www.google-analytics.com/analytics.js', {
use: () => ({ gtag: window.gtag })
})
// Now use any feature of Google's gtag() function as you wish
// Instead of writing complex code to find and check window.gtag
```
+If you don't want to install the Nuxt Scripts module, you can still use the uderlying native `useScript` method. You will need to `import { useScript } from '@unhead/vue'` in order to use it.
### The `useHead` composable
diff --git a/package.json b/package.json
index a94f13c5..93c01330 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "nuxt-security",
- "version": "2.0.0-rc.9",
+ "version": "2.0.0",
"license": "MIT",
"type": "module",
"homepage": "https://nuxt-security.vercel.app",
@@ -56,19 +56,19 @@
"defu": "^6.1.1",
"nuxt-csurf": "^1.5.1",
"pathe": "^1.0.0",
- "unplugin-remove": "^1.0.2",
+ "unplugin-remove": "^1.0.3",
"xss": "^1.0.14"
},
"devDependencies": {
"@nuxt/eslint-config": "^0.3.10",
- "@nuxt/module-builder": "^0.6.0",
+ "@nuxt/module-builder": "^0.8.3",
"@nuxt/schema": "^3.11.2",
"@nuxt/test-utils": "^3.12.0",
"@types/node": "^18.18.1",
"eslint": "^8.50.0",
"nuxt": "^3.11.2",
- "vitest": "^1.3.1",
- "typescript": "^5.4.5"
+ "typescript": "^5.4.5",
+ "vitest": "^1.3.1"
},
"stackblitz": {
"installDependencies": false,
diff --git a/playground/components/ServerComponent.server.vue b/playground/components/ServerComponent.server.vue
new file mode 100644
index 00000000..ad7324ed
--- /dev/null
+++ b/playground/components/ServerComponent.server.vue
@@ -0,0 +1,10 @@
+
+
+
Server-only Nuxt-Island component
+
Nonce: {{ nonce }}
+
+
+
+
\ No newline at end of file
diff --git a/playground/nuxt.config.ts b/playground/nuxt.config.ts
index b76436a2..ecd18c12 100644
--- a/playground/nuxt.config.ts
+++ b/playground/nuxt.config.ts
@@ -48,7 +48,6 @@ export default defineNuxtConfig({
// Global configuration
security: {
headers: {
- crossOriginEmbedderPolicy: false,
xXSSProtection: '0'
},
rateLimiter: {
diff --git a/playground/pages/island.vue b/playground/pages/island.vue
new file mode 100644
index 00000000..9f18b75e
--- /dev/null
+++ b/playground/pages/island.vue
@@ -0,0 +1,6 @@
+
+
+ Island Page
+
+
+
\ No newline at end of file
diff --git a/src/defaultConfig.ts b/src/defaultConfig.ts
index 5b286d43..b04a5ad0 100644
--- a/src/defaultConfig.ts
+++ b/src/defaultConfig.ts
@@ -9,7 +9,7 @@ export const defaultSecurityConfig = (serverlUrl: string, strict: boolean) => {
headers: {
crossOriginResourcePolicy: 'same-origin',
crossOriginOpenerPolicy: 'same-origin',
- crossOriginEmbedderPolicy: 'credentialless',
+ crossOriginEmbedderPolicy: process.env.NODE_ENV === 'development' ? 'unsafe-none' : 'credentialless',
contentSecurityPolicy: {
'base-uri': ["'none'"],
'font-src': ["'self'", 'https:', 'data:'],
diff --git a/src/runtime/nitro/plugins/40-cspSsrNonce.ts b/src/runtime/nitro/plugins/40-cspSsrNonce.ts
index ba97c305..e796de65 100644
--- a/src/runtime/nitro/plugins/40-cspSsrNonce.ts
+++ b/src/runtime/nitro/plugins/40-cspSsrNonce.ts
@@ -1,5 +1,5 @@
import { defineNitroPlugin } from '#imports'
-import crypto from 'node:crypto'
+import { randomBytes } from 'node:crypto'
import { resolveSecurityRules } from '../context'
const LINK_RE = /]*?>)/gi
@@ -17,18 +17,32 @@ export default defineNitroPlugin((nitroApp) => {
return
}
+ // Genearate a 16-byte random nonce for each request.
nitroApp.hooks.hook('request', (event) => {
+ if (event.context.security?.nonce) {
+ // When rendering server-only (NuxtIsland) components, each component will trigger a request event.
+ // The request context is shared between the event that renders the actual page and the island request events.
+ // Make sure to only generate the nonce once.
+ return
+ }
+
const rules = resolveSecurityRules(event)
if (rules.enabled && rules.nonce && !import.meta.prerender) {
- const nonce = crypto.randomBytes(16).toString('base64')
+ const nonce = randomBytes(16).toString('base64')
event.context.security!.nonce = nonce
}
})
+ // Set the nonce attribute on all script, style, and link tags.
nitroApp.hooks.hook('render:html', (html, { event }) => {
// Exit if no CSP defined
const rules = resolveSecurityRules(event)
- if (!rules.enabled || !rules.headers || !rules.headers.contentSecurityPolicy || !rules.nonce) {
+ if (
+ !rules.enabled ||
+ !rules.headers ||
+ !rules.headers.contentSecurityPolicy ||
+ !rules.nonce
+ ) {
return
}
@@ -37,21 +51,28 @@ export default defineNitroPlugin((nitroApp) => {
type Section = 'body' | 'bodyAppend' | 'bodyPrepend' | 'head'
const sections = ['body', 'bodyAppend', 'bodyPrepend', 'head'] as Section[]
for (const section of sections) {
- html[section] = html[section].map(element => {
+ html[section] = html[section].map((element) => {
// Add nonce to all link tags
- element = element.replace(LINK_RE, (match, rest)=>{
+ element = element.replace(LINK_RE, (match, rest) => {
return `{
+ element = element.replace(SCRIPT_RE, (match, rest) => {
return `
\ No newline at end of file
diff --git a/test/fixtures/ssrNonce/pages/server-component.vue b/test/fixtures/ssrNonce/pages/server-component.vue
new file mode 100644
index 00000000..ef3098ad
--- /dev/null
+++ b/test/fixtures/ssrNonce/pages/server-component.vue
@@ -0,0 +1,5 @@
+
+