Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Chore/2.0.0 #492

Open
wants to merge 23 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
ff0ecfe
feat(csp): support style nonce in development
dargmuesli Jun 12, 2024
fad91ee
Update from useScript to Nuxt Scripts
vejja Jul 1, 2024
338be11
feat-#487: local dev with nuxt devtools
Baroshem Jul 2, 2024
b7701f1
Merge pull request #475 from dargmuesli/feat/csp/vite
Baroshem Jul 16, 2024
88dbb4c
Merge pull request #488 from Baroshem/feat/#487
Baroshem Jul 16, 2024
2d0ae0a
Merge pull request #485 from Baroshem/vejja-patch-3
Baroshem Jul 16, 2024
4528880
fix: ensure RegExp[] origin can be passed to appSecurityOptions
Shana-AE Jul 22, 2024
765d7e1
Merge pull request #498 from Shana-AE/fix/regexp-corsHanlder.origin
Baroshem Jul 26, 2024
23af05a
test: use nullish coalescing operator
P4sca1 Jul 31, 2024
eb097d0
test: add test cases for server-only components
P4sca1 Jul 31, 2024
c38a710
fix: log warning when removing static nonce from CSP header
P4sca1 Jul 31, 2024
2b0cf0f
fix: skip nonce generation and csp header update for NuxtIsland requests
P4sca1 Jul 31, 2024
a2425ce
docs: update information about Nuxt Image
P4sca1 Jul 31, 2024
0e3ab07
chore: fix typo
P4sca1 Jul 31, 2024
7811a00
Merge pull request #503 from P4sca1/docs/image-faq
Baroshem Aug 1, 2024
b0b4a08
merge changes from #500 into #502
vejja Aug 2, 2024
57ff90b
Replace isIslandRequest util with check if nonce already exist
P4sca1 Aug 3, 2024
1a5ada9
fix: use console warn instead of useLogger
P4sca1 Aug 3, 2024
e6df1ac
Merge pull request #502 from P4sca1/main
Baroshem Aug 6, 2024
4993963
feat: bump unplugin-remove to fix sitemaps
Baroshem Aug 8, 2024
b133ed6
Revert "fix: ensure RegExp[] origin can be passed to appSecurityOptions"
Baroshem Aug 9, 2024
f613df5
fix: update to latest @nuxt/module-builder
ThibaultVlacich Sep 9, 2024
a359071
Merge pull request #516 from ThibaultVlacich/update-module-builder
Baroshem Sep 9, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 0 additions & 15 deletions docs/content/1.documentation/1.getting-started/1.setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -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',
},
},
});
```
31 changes: 0 additions & 31 deletions docs/content/1.documentation/2.headers/1.csp.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"}
Copy link
Collaborator

@vejja vejja Jul 30, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this section should not be removed
The vite nonce is only inserted in dev mode, if I understand correctly
@dargmuesli is this true?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think all offending actions are only executed in dev mode as well. Styles should not be added through JavaScript by Nuxt in production as far as I know. They're just referenced as compiled CSS content in production, at least without changes to any configuration.
I have the fix contained in this PR running for a few of my sites for quite some time now and have not observed any issues (even with close monitoring done by Sentry).

Maybe @danielroe could quickly confirm or refute that style loading through JavaScript only happens in dev mode by vite and, by default, not by Nuxt in production?

Copy link
Collaborator

@vejja vejja Jul 31, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm pretty sure we had to use unsafe-inline in styles in our own doc website, because @nuxt/ui uses pinceau under the hood, and pinceau modifies styles dynamically at runtime via Javascript.

Copy link
Contributor

@dargmuesli dargmuesli Aug 1, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, but that's somewhat 3rd party, no? The section talks about "Nuxt's mechanism for Client-Side hydration of styles" so maybe the wording should be changed then to reflect the actual reason for the recommendation better. I'd still say the recommendation doesn't really need to be that prominent for the pinceau use case though, but that's just an opinion.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll unsubscribe from this PR to reduce noise, just tag me again if you need me

Our default recommendation is to avoid using the `"'nonce-{{nonce}}'"` placeholder on `style-src` policy.
<br>
⚠ This is because Nuxt's mechanism for Client-Side hydration of styles could be blocked by CSP in that case.
<br>
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`._


Expand Down Expand Up @@ -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.
Expand Down
39 changes: 5 additions & 34 deletions docs/content/1.documentation/5.advanced/2.faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
<template>
<NuxtImg src="https://localhost:8000/api/image/xyz" :nonce="nonce" />
</template>

<script lang="ts" setup>
const nonce = useNonce()
</script>
```
When using `<NuxtImg>` or `<NuxtPicture>`, 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.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @P4sca1
Isn’t it a bit severe ?
You could probably use ‘unsafe-hashes’ here, and the inline code is always the same so you could pre-hash it.
I do agree this is not ideal though. @harlan-zw was able to replace all inline event handlers with addEventListener in @nuxt/scripts so maybe the team at NuxtImg can use the same approach?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The inline code will indeed be always the same, so using unsafe-hashes could work. Maybe we could add it by default in non strict mode or behind a feature flag in strict mode to support <NuxtImg> and <NuxtPicture>.

I calculated the hash that would be needed:

echo -n "this.setAttribute('data-error', 1)" | openssl sha256 -binary | openssl base64
bwK6T5wZVTANitXbrTsel7kl/PyCjCd/Dq5Qoz3imjM=

Using addEventListener in this case is not trivial, because the event listener would be attached in onMounted(), which is too late for some kind of errors. So some errors, e.g. when the url is invalid, could be missed.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @P4sca1
What is the issue when CSP denies execution of the error handler?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The data-error property does not get set on the image tag and the error event is never emitted.
From a user's perspective the page works fine, it just shows an unloaded image.

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
Expand Down
18 changes: 9 additions & 9 deletions docs/content/1.documentation/5.advanced/3.strict-csp.md
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -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

Expand Down
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
10 changes: 10 additions & 0 deletions playground/components/ServerComponent.server.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<template>
<div>
<h1>Server-only Nuxt-Island component</h1>
<p>Nonce: <span id="server-nonce">{{ nonce }}</span></p>
</div>
</template>

<script setup lang="ts">
const nonce = useNonce()
</script>
1 change: 0 additions & 1 deletion playground/nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ export default defineNuxtConfig({
// Global configuration
security: {
headers: {
crossOriginEmbedderPolicy: false,
xXSSProtection: '0'
},
rateLimiter: {
Expand Down
6 changes: 6 additions & 0 deletions playground/pages/island.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<template>
<div>
Island Page
<ServerComponent />
</div>
</template>
2 changes: 1 addition & 1 deletion src/defaultConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:'],
Expand Down
35 changes: 28 additions & 7 deletions src/runtime/nitro/plugins/40-cspSsrNonce.ts
Original file line number Diff line number Diff line change
@@ -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 = /<link([^>]*?>)/gi
Expand All @@ -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
}

Expand All @@ -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 `<link nonce="${nonce}"` + rest
})
// Add nonce to all script tags
element = element.replace(SCRIPT_RE, (match, rest)=>{
element = element.replace(SCRIPT_RE, (match, rest) => {
return `<script nonce="${nonce}"` + rest
})
// Add nonce to all style tags
element = element.replace(STYLE_RE, (match, rest)=>{
element = element.replace(STYLE_RE, (match, rest) => {
return `<style nonce="${nonce}"` + rest
})
return element
})
}

// Add meta header for Vite in development
if (import.meta.dev) {
html.head.push(
`<meta property="csp-nonce" nonce="${nonce}">`,
)
}
})
})
14 changes: 13 additions & 1 deletion src/runtime/nitro/plugins/50-updateCsp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ import type { ContentSecurityPolicyValue } from '../../../types/headers'
*/
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('render:html', (response, { event }) => {
if (response.island) {
// When rendering server-only (NuxtIsland) components, do not update CSP headers.
// The CSP headers from the page that the island components are mounted into are used.
return
}

const rules = resolveSecurityRules(event)
if (rules.enabled && rules.headers) {
const headers = rules.headers
Expand All @@ -31,7 +37,13 @@ function updateCspVariables(csp: ContentSecurityPolicyValue, nonce?: string, scr
// Make sure nonce placeholders are eliminated
const sources = (typeof value === 'string') ? value.split(' ').map(token => token.trim()).filter(token => token) : value
const modifiedSources = sources
.filter(source => !source.startsWith("'nonce-") || source === "'nonce-{{nonce}}'")
.filter(source => {
if (source.startsWith("'nonce-") && source !== "'nonce-{{nonce}}'") {
console.warn('[nuxt-security] removing static nonce from CSP header')
return false
}
return true
})
.map(source => {
if (source === "'nonce-{{nonce}}'") {
return nonce ? `'nonce-${nonce}'` : ''
Expand Down
10 changes: 6 additions & 4 deletions src/runtime/nitro/plugins/60-recombineHtml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,13 @@ export default defineNitroPlugin((nitroApp) => {

// Let's insert the CSP meta tag just after the first tag which should be the charset meta
let insertIndex = 0
const metaCharsetMatch = html.head[0].match(/^<meta charset="(.*?)">/mdi)
if (metaCharsetMatch && metaCharsetMatch.indices) {
insertIndex = metaCharsetMatch.indices[0][1]
if (html.head.length > 0) {
const metaCharsetMatch = html.head[0].match(/^<meta charset="(.*?)">/mdi)
if (metaCharsetMatch && metaCharsetMatch.indices) {
insertIndex = metaCharsetMatch.indices[0][1]
}
html.head[0] = html.head[0].slice(0, insertIndex) + `<meta http-equiv="Content-Security-Policy" content="${headerValue}">` + html.head[0].slice(insertIndex)
}
html.head[0] = html.head[0].slice(0, insertIndex) + `<meta http-equiv="Content-Security-Policy" content="${headerValue}">` + html.head[0].slice(insertIndex)
}
})
})
10 changes: 10 additions & 0 deletions test/fixtures/ssrNonce/components/ServerComponent.server.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<template>
<div>
<h1>Server-only Nuxt-Island component</h1>
<p>Nonce: <span id="server-nonce">{{ nonce }}</span></p>
</div>
</template>

<script setup lang="ts">
const nonce = useNonce()
</script>
5 changes: 5 additions & 0 deletions test/fixtures/ssrNonce/pages/server-component.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<template>
<div>
<ServerComponent />
</div>
</template>
Loading
Loading