Skip to content

Commit 6005b46

Browse files
authored
Merge pull request #492 from Baroshem/chore/2.0.0
Chore/2.0.0
2 parents 2d51282 + 4c577d1 commit 6005b46

31 files changed

+612
-146
lines changed

docs/content/1.documentation/1.getting-started/1.setup.md

-15
Original file line numberDiff line numberDiff line change
@@ -24,18 +24,3 @@ security: {
2424
```
2525

2626
You can find more about configuring `nuxt-security` [here](/documentation/getting-started/configuration).
27-
28-
## Using with Nuxt DevTools
29-
30-
In order to make this module work with Nuxt DevTools add following configuration to your projects:
31-
32-
```js{}[nuxt.config.ts]
33-
export default defineNuxtConfig({
34-
modules: ['nuxt-security', '@nuxt/devtools'],
35-
security: {
36-
headers: {
37-
crossOriginEmbedderPolicy: process.env.NODE_ENV === 'development' ? 'unsafe-none' : 'require-corp',
38-
},
39-
},
40-
});
41-
```

docs/content/1.documentation/2.headers/1.csp.md

-31
Original file line numberDiff line numberDiff line change
@@ -215,15 +215,6 @@ export default defineNuxtConfig({
215215
- `"'nonce-{{nonce}}'"` placeholder: Include this value in any individual policy that you want to be governed by nonce.
216216

217217

218-
::alert{type="warning"}
219-
Our default recommendation is to avoid using the `"'nonce-{{nonce}}'"` placeholder on `style-src` policy.
220-
<br>
221-
⚠ This is because Nuxt's mechanism for Client-Side hydration of styles could be blocked by CSP in that case.
222-
<br>
223-
For further discussion and alternatives, please refer to our [Advanced Section on Strict CSP](/documentation/advanced/strict-csp).
224-
::
225-
226-
227218
_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`._
228219

229220

@@ -304,28 +295,6 @@ Please see below our section on [Integrity Hashes For SSG](#integrity-hashes-for
304295
_Note: Hashes only work for SSG. The `ssg` options are ignored when you build your app for SSR via `nuxi build`._
305296

306297

307-
308-
## Hot reload during development
309-
310-
If you have enabled `nonce-{{nonce}}` on `style-src`, you will need to disable it in order to allow hot reloading during development.
311-
312-
```ts
313-
export default defineNuxtConfig({
314-
security: {
315-
nonce: true,
316-
headers: {
317-
contentSecurityPolicy: {
318-
'style-src': process.env.NODE_ENV === 'development' ?
319-
["'self'", "'unsafe-inline'"] :
320-
["'self'", "'unsafe-inline'", "nonce-{{nonce}}"]
321-
}
322-
}
323-
}
324-
})
325-
```
326-
327-
Note that this is not necessary if you use our default configuration settings.
328-
329298
## Per-route configuration
330299

331300
All Content Security Policy options can be defined on a per-route level.

docs/content/1.documentation/3.middleware/4.cors-handler.md

+13-3
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,9 @@ You can also disable the middleware globally or per route by setting `corsHandle
4848
CORS handler accepts following configuration options:
4949

5050
```ts
51-
interface H3CorsOptions {
52-
origin?: '*' | 'null' | (string | RegExp)[] | ((origin: string) => boolean);
51+
interface CorsOptions = {
52+
origin?: '*' | string | string[];
53+
useRegExp?: boolean;
5354
methods?: '*' | HTTPMethod[];
5455
allowHeaders?: '*' | string[];
5556
exposeHeaders?: '*' | string[];
@@ -65,7 +66,16 @@ interface H3CorsOptions {
6566

6667
- Default: `${serverUrl}`
6768

68-
The Access-Control-Allow-Origin response header indicates whether the response can be shared with requesting code from the given origin.
69+
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.
70+
71+
### `useRegExp`
72+
73+
Set to `true` to parse all origin values into a regular expression using `new RegExp(origin, 'i')`.
74+
You cannot use RegExp instances directly as origin values, because the nuxt config needs to be serializable.
75+
When using regular expressions, make sure to escape dots in origins correctly. Otherwise a dot will match every character.
76+
77+
The following matches `https://1.foo.example.com`, `https://a.b.c.foo.example.com`, but not `https://foo.example.com`.
78+
`'(.*)\\.foo.example\\.com'`
6979

7080
### `methods`
7181

docs/content/1.documentation/5.advanced/2.faq.md

+5-34
Original file line numberDiff line numberDiff line change
@@ -245,43 +245,14 @@ Next, you need to configure your img tag to include the `crossorigin` attribute:
245245
ℹ Read more about it [here](https://github.com/Baroshem/nuxt-security/issues/138#issuecomment-1497883915).
246246
::
247247

248-
## Using nonce with CSP for Nuxt Image
248+
## Nuxt Image
249249

250-
Having securely configured images is crucial for modern web applications. Check out how to do it below:
251-
252-
```ts
253-
// nuxt.config.ts
254-
255-
security: {
256-
nonce: true,
257-
headers: {
258-
contentSecurityPolicy: {
259-
'img-src': ["'self'", 'data:', 'https:'],
260-
'script-src': [
261-
"'self'", // backwards compatibility for older browsers that don't support strict-dynamic
262-
"'nonce-{{nonce}}'",
263-
"'strict-dynamic'"
264-
],
265-
'script-src-attr': ["'self'"]
266-
}
267-
}
268-
}
269-
```
270-
271-
And then configure `NuxtImg` like following:
272-
273-
```vue
274-
<template>
275-
<NuxtImg src="https://localhost:8000/api/image/xyz" :nonce="nonce" />
276-
</template>
277-
278-
<script lang="ts" setup>
279-
const nonce = useNonce()
280-
</script>
281-
```
250+
When using `<NuxtImg>` or `<NuxtPicture>`, an inline script will be used for error handling during SSR.
251+
This will lead to CSP issues if `unsafe-inline` is not allowed and the image fails to load.
252+
Using nonces for inline event handlers is not supported, so currently there is no workaround.
282253

283254
::alert{type="info"}
284-
ℹ Read more about it [here](https://github.com/Baroshem/nuxt-security/issues/218#issuecomment-1736940913).
255+
ℹ Read more about it [here](https://github.com/nuxt/image/issues/1011#issuecomment-2242761992).
285256
::
286257

287258
## Issue on Firefox when using IFrame

docs/content/1.documentation/5.advanced/3.strict-csp.md

+9-9
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,7 @@ export defaultNuxtConfig({
312312
313313
### The `useScript` composable
314314
315-
Starting from Nuxt 3.11, it is possible to insert any external script in one single line with the new `useScript` composable.
315+
The Nuxt Scripts module allows you to insert any external script in one single line with its `useScript` composable.
316316
317317
```ts
318318
useScript('https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js')
@@ -324,24 +324,24 @@ The `useScript` method has several key features:
324324
- It does not insert inline event handlers, therefore CSP will never block the script from executing after load
325325
- 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
326326
327-
For all of these reasons, we strongly recommend `useScript` as the best way to load your external scripts in a CSP-compatible way.
327+
In addition, Nuxt Scripts provide easy integration of `useScript` into any Nuxt application:
328+
- A number of standard scripts are already pre-packaged
329+
- You can load your scripts globally in `nuxt.config.ts`
330+
- `useScript` is auto-imported
328331
329-
The `unjs/unhead` repo has a [detailed section here](https://unhead.unjs.io/usage/composables/use-script) on how to use `useScript`.
332+
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.
330333
331-
Check out their examples and find out how easy it is to include Google Analytics in your application:
334+
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:
332335
333336
```ts
334-
import { useScript } from 'unhead'
335-
336-
const { gtag } = useScript({
337-
src: 'https://www.google-analytics.com/analytics.js',
338-
}, {
337+
const { gtag } = useScript('https://www.google-analytics.com/analytics.js', {
339338
use: () => ({ gtag: window.gtag })
340339
})
341340
// Now use any feature of Google's gtag() function as you wish
342341
// Instead of writing complex code to find and check window.gtag
343342
```
344343
344+
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.
345345
346346
### The `useHead` composable
347347

package.json

+5-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "nuxt-security",
3-
"version": "2.0.0-rc.9",
3+
"version": "2.0.0",
44
"license": "MIT",
55
"type": "module",
66
"homepage": "https://nuxt-security.vercel.app",
@@ -56,19 +56,19 @@
5656
"defu": "^6.1.1",
5757
"nuxt-csurf": "^1.5.1",
5858
"pathe": "^1.0.0",
59-
"unplugin-remove": "^1.0.2",
59+
"unplugin-remove": "^1.0.3",
6060
"xss": "^1.0.14"
6161
},
6262
"devDependencies": {
6363
"@nuxt/eslint-config": "^0.3.10",
64-
"@nuxt/module-builder": "^0.6.0",
64+
"@nuxt/module-builder": "^0.8.3",
6565
"@nuxt/schema": "^3.11.2",
6666
"@nuxt/test-utils": "^3.12.0",
6767
"@types/node": "^18.18.1",
6868
"eslint": "^8.50.0",
6969
"nuxt": "^3.11.2",
70-
"vitest": "^1.3.1",
71-
"typescript": "^5.4.5"
70+
"typescript": "^5.4.5",
71+
"vitest": "^1.3.1"
7272
},
7373
"stackblitz": {
7474
"installDependencies": false,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<template>
2+
<div>
3+
<h1>Server-only Nuxt-Island component</h1>
4+
<p>Nonce: <span id="server-nonce">{{ nonce }}</span></p>
5+
</div>
6+
</template>
7+
8+
<script setup lang="ts">
9+
const nonce = useNonce()
10+
</script>

playground/nuxt.config.ts

-1
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@ export default defineNuxtConfig({
4848
// Global configuration
4949
security: {
5050
headers: {
51-
crossOriginEmbedderPolicy: false,
5251
xXSSProtection: '0'
5352
},
5453
rateLimiter: {

playground/pages/island.vue

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<template>
2+
<div>
3+
Island Page
4+
<ServerComponent />
5+
</div>
6+
</template>

src/defaultConfig.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export const defaultSecurityConfig = (serverlUrl: string, strict: boolean) => {
99
headers: {
1010
crossOriginResourcePolicy: 'same-origin',
1111
crossOriginOpenerPolicy: 'same-origin',
12-
crossOriginEmbedderPolicy: 'credentialless',
12+
crossOriginEmbedderPolicy: process.env.NODE_ENV === 'development' ? 'unsafe-none' : 'credentialless',
1313
contentSecurityPolicy: {
1414
'base-uri': ["'none'"],
1515
'font-src': ["'self'", 'https:', 'data:'],

src/runtime/nitro/plugins/40-cspSsrNonce.ts

+28-7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { defineNitroPlugin } from '#imports'
2-
import crypto from 'node:crypto'
2+
import { randomBytes } from 'node:crypto'
33
import { resolveSecurityRules } from '../context'
44

55
const LINK_RE = /<link([^>]*?>)/gi
@@ -17,18 +17,32 @@ export default defineNitroPlugin((nitroApp) => {
1717
return
1818
}
1919

20+
// Genearate a 16-byte random nonce for each request.
2021
nitroApp.hooks.hook('request', (event) => {
22+
if (event.context.security?.nonce) {
23+
// When rendering server-only (NuxtIsland) components, each component will trigger a request event.
24+
// The request context is shared between the event that renders the actual page and the island request events.
25+
// Make sure to only generate the nonce once.
26+
return
27+
}
28+
2129
const rules = resolveSecurityRules(event)
2230
if (rules.enabled && rules.nonce && !import.meta.prerender) {
23-
const nonce = crypto.randomBytes(16).toString('base64')
31+
const nonce = randomBytes(16).toString('base64')
2432
event.context.security!.nonce = nonce
2533
}
2634
})
2735

36+
// Set the nonce attribute on all script, style, and link tags.
2837
nitroApp.hooks.hook('render:html', (html, { event }) => {
2938
// Exit if no CSP defined
3039
const rules = resolveSecurityRules(event)
31-
if (!rules.enabled || !rules.headers || !rules.headers.contentSecurityPolicy || !rules.nonce) {
40+
if (
41+
!rules.enabled ||
42+
!rules.headers ||
43+
!rules.headers.contentSecurityPolicy ||
44+
!rules.nonce
45+
) {
3246
return
3347
}
3448

@@ -37,21 +51,28 @@ export default defineNitroPlugin((nitroApp) => {
3751
type Section = 'body' | 'bodyAppend' | 'bodyPrepend' | 'head'
3852
const sections = ['body', 'bodyAppend', 'bodyPrepend', 'head'] as Section[]
3953
for (const section of sections) {
40-
html[section] = html[section].map(element => {
54+
html[section] = html[section].map((element) => {
4155
// Add nonce to all link tags
42-
element = element.replace(LINK_RE, (match, rest)=>{
56+
element = element.replace(LINK_RE, (match, rest) => {
4357
return `<link nonce="${nonce}"` + rest
4458
})
4559
// Add nonce to all script tags
46-
element = element.replace(SCRIPT_RE, (match, rest)=>{
60+
element = element.replace(SCRIPT_RE, (match, rest) => {
4761
return `<script nonce="${nonce}"` + rest
4862
})
4963
// Add nonce to all style tags
50-
element = element.replace(STYLE_RE, (match, rest)=>{
64+
element = element.replace(STYLE_RE, (match, rest) => {
5165
return `<style nonce="${nonce}"` + rest
5266
})
5367
return element
5468
})
5569
}
70+
71+
// Add meta header for Vite in development
72+
if (import.meta.dev) {
73+
html.head.push(
74+
`<meta property="csp-nonce" nonce="${nonce}">`,
75+
)
76+
}
5677
})
5778
})

src/runtime/nitro/plugins/50-updateCsp.ts

+13-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ import type { ContentSecurityPolicyValue } from '../../../types/headers'
77
*/
88
export default defineNitroPlugin((nitroApp) => {
99
nitroApp.hooks.hook('render:html', (response, { event }) => {
10+
if (response.island) {
11+
// When rendering server-only (NuxtIsland) components, do not update CSP headers.
12+
// The CSP headers from the page that the island components are mounted into are used.
13+
return
14+
}
15+
1016
const rules = resolveSecurityRules(event)
1117
if (rules.enabled && rules.headers) {
1218
const headers = rules.headers
@@ -31,7 +37,13 @@ function updateCspVariables(csp: ContentSecurityPolicyValue, nonce?: string, scr
3137
// Make sure nonce placeholders are eliminated
3238
const sources = (typeof value === 'string') ? value.split(' ').map(token => token.trim()).filter(token => token) : value
3339
const modifiedSources = sources
34-
.filter(source => !source.startsWith("'nonce-") || source === "'nonce-{{nonce}}'")
40+
.filter(source => {
41+
if (source.startsWith("'nonce-") && source !== "'nonce-{{nonce}}'") {
42+
console.warn('[nuxt-security] removing static nonce from CSP header')
43+
return false
44+
}
45+
return true
46+
})
3547
.map(source => {
3648
if (source === "'nonce-{{nonce}}'") {
3749
return nonce ? `'nonce-${nonce}'` : ''

src/runtime/nitro/plugins/60-recombineHtml.ts

+6-4
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,13 @@ export default defineNitroPlugin((nitroApp) => {
2424

2525
// Let's insert the CSP meta tag just after the first tag which should be the charset meta
2626
let insertIndex = 0
27-
const metaCharsetMatch = html.head[0].match(/^<meta charset="(.*?)">/mdi)
28-
if (metaCharsetMatch && metaCharsetMatch.indices) {
29-
insertIndex = metaCharsetMatch.indices[0][1]
27+
if (html.head.length > 0) {
28+
const metaCharsetMatch = html.head[0].match(/^<meta charset="(.*?)">/mdi)
29+
if (metaCharsetMatch && metaCharsetMatch.indices) {
30+
insertIndex = metaCharsetMatch.indices[0][1]
31+
}
32+
html.head[0] = html.head[0].slice(0, insertIndex) + `<meta http-equiv="Content-Security-Policy" content="${headerValue}">` + html.head[0].slice(insertIndex)
3033
}
31-
html.head[0] = html.head[0].slice(0, insertIndex) + `<meta http-equiv="Content-Security-Policy" content="${headerValue}">` + html.head[0].slice(insertIndex)
3234
}
3335
})
3436
})

0 commit comments

Comments
 (0)