Skip to content

Commit 11dc2d5

Browse files
authored
Merge pull request #347 from Baroshem/chore/1.1.0
Chore/1.1.0
2 parents ef27a76 + e485692 commit 11dc2d5

31 files changed

+335
-71
lines changed

.stackblitz/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,6 @@
1111
"nuxt": "3.9.3"
1212
},
1313
"dependencies": {
14-
"nuxt-security": "^1.0.1"
14+
"nuxt-security": "^1.1.0"
1515
}
1616
}

.stackblitz/yarn.lock

+13-25
Original file line numberDiff line numberDiff line change
@@ -1183,7 +1183,7 @@
11831183
dependencies:
11841184
"@rollup/pluginutils" "^5.0.2"
11851185

1186-
"@rollup/pluginutils@^4.0.0", "@rollup/pluginutils@^4.2.1":
1186+
"@rollup/pluginutils@^4.0.0":
11871187
version "4.2.1"
11881188
resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-4.2.1.tgz#e6c6c3aba0744edce3fb2074922d3776c0af2a6d"
11891189
integrity sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==
@@ -3574,13 +3574,6 @@ magic-string-ast@^0.3.0:
35743574
dependencies:
35753575
magic-string "^0.30.2"
35763576

3577-
magic-string@^0.26.3:
3578-
version "0.26.7"
3579-
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.26.7.tgz#caf7daf61b34e9982f8228c4527474dac8981d6f"
3580-
integrity sha512-hX9XH3ziStPoPhJxLq1syWuZMxbDvGNbVchfrdCtanC7D13888bMFow61x8axrx+GfHLtVeAx2kxL7tTGRl+Ow==
3581-
dependencies:
3582-
sourcemap-codec "^1.4.8"
3583-
35843577
magic-string@^0.30.0, magic-string@^0.30.2, magic-string@^0.30.3, magic-string@^0.30.4, magic-string@^0.30.5:
35853578
version "0.30.5"
35863579
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.5.tgz#1994d980bd1c8835dc6e78db7cbd4ae4f24746f9"
@@ -4108,18 +4101,18 @@ nuxt-csurf@^1.3.1:
41084101
defu "^6.1.1"
41094102
uncsrf "^1.1.1"
41104103

4111-
nuxt-security@^1.0.1:
4112-
version "1.0.1"
4113-
resolved "https://registry.yarnpkg.com/nuxt-security/-/nuxt-security-1.0.1.tgz#a98a5b8636e46c6ff97bc7ec104d03b4c58aee0f"
4114-
integrity sha512-GT504TFIJn0sM6yhdiUIf5EXoakYir0zs/2KO4uLqcP6XyX7FDj/SW0D3r8EvK80peL2dPvcWRmYRaqo1vPU2g==
4104+
nuxt-security@^1.1.0:
4105+
version "1.1.0"
4106+
resolved "https://registry.yarnpkg.com/nuxt-security/-/nuxt-security-1.1.0.tgz#ccb5600cbb835a4523fe22f89b7fd23a6544a287"
4107+
integrity sha512-nsvdUQbHjpGPNRUYi+ZDB5yUGL7rjTIhTxpuZwqxJuOPwAbw19S2DlgE+HTxn9CmzTCv8231lEhpD5G/Y4uG7g==
41154108
dependencies:
41164109
"@nuxt/kit" "^3.8.0"
41174110
basic-auth "^2.0.1"
41184111
cheerio "^1.0.0-rc.12"
41194112
defu "^6.1.1"
41204113
nuxt-csurf "^1.3.1"
41214114
pathe "^1.0.0"
4122-
unplugin-remove "^0.1.6"
4115+
unplugin-remove "^0.1.7"
41234116
xss "^1.0.14"
41244117

41254118
@@ -5101,11 +5094,6 @@ source-map@^0.7.4:
51015094
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.4.tgz#a9bbe705c9d8846f4e08ff6765acf0f1b0898656"
51025095
integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==
51035096

5104-
sourcemap-codec@^1.4.8:
5105-
version "1.4.8"
5106-
resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4"
5107-
integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==
5108-
51095097
spdx-correct@^3.0.0:
51105098
version "3.2.0"
51115099
resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.2.0.tgz#4f5ab0668f0059e34f9c00dce331784a12de4e9c"
@@ -5517,14 +5505,14 @@ universalify@^2.0.0:
55175505
resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717"
55185506
integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==
55195507

5520-
unplugin-remove@^0.1.6:
5521-
version "0.1.6"
5522-
resolved "https://registry.yarnpkg.com/unplugin-remove/-/unplugin-remove-0.1.6.tgz#0b3d0a77ef2061de8a85cc239a5ba7f5c64d535d"
5523-
integrity sha512-/jwD4+ZzeBGC32Rx7m59FOhqALmtLsTeabFwaYM8yQMVaVO8un8AQxZi3YFJirPzJgEW43e5/wQpze8z/WwOxA==
5508+
unplugin-remove@^0.1.7:
5509+
version "0.1.7"
5510+
resolved "https://registry.yarnpkg.com/unplugin-remove/-/unplugin-remove-0.1.7.tgz#0ee1b14963a6c186f5f263224b7085bf260913a5"
5511+
integrity sha512-7BaEfgR/hMQRgaN++RAILeq9/wBrJPqCLRsQH+ow8979s2TE3TAFE4rQRmMUPcJ/w4ccsVSLepwGbimLMkQjyA==
55245512
dependencies:
5525-
"@rollup/pluginutils" "^4.2.1"
5526-
magic-string "^0.26.3"
5527-
unplugin "^1.5.0"
5513+
"@rollup/pluginutils" "^5.1.0"
5514+
magic-string "^0.30.5"
5515+
unplugin "^1.5.1"
55285516

55295517
unplugin-vue-router@^0.7.0:
55305518
version "0.7.0"

docs/content/0.index.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ cta:
9292
Nuxt Security solves several security issues automatically by implementing Headers and Middleware accordingly to OWASP & OWASP Top 10 documents. For others, it provides optional middleware that will help you handle more advanced cases like Cross Site Request Forgery.
9393

9494
#support
95-
:video-player{src="https://www.youtube.com/watch?v=8RDPrptc9uU"}
95+
:video-player{src="https://www.youtube.com/watch?v=sJVeU0KGmv4"}
9696
::
9797

9898

docs/content/1.documentation/1.getting-started/2.configuration.md

-1
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,6 @@ security: {
9898
preflight: {
9999
statusCode: 204
100100
},
101-
throwError: true
102101
},
103102
allowedMethodsRestricter: {
104103
methods: '*',

docs/content/1.documentation/1.getting-started/3.usage.md

+34
Original file line numberDiff line numberDiff line change
@@ -169,3 +169,37 @@ export default defineNuxtConfig({
169169
}
170170
})
171171
```
172+
173+
## Runtime configuration
174+
175+
If you need to change the headers configuration at runtime, it is possible to do it through `nuxt-security:headers` hook.
176+
177+
### Enabling the option
178+
179+
This feature is optional, you can enable it with
180+
181+
```ts
182+
export default defineNuxtConfig({
183+
modules: ['nuxt-security'],
184+
security: {
185+
runtimeHooks: true
186+
}
187+
})
188+
```
189+
190+
### Usage
191+
192+
Within your nitro plugin. You can override the previous configuration of a route with `nuxt-security:headers`.
193+
194+
```ts
195+
export default defineNitroPlugin((nitroApp) => {
196+
nitroApp.hooks.hook('nuxt-security:ready', () => {
197+
nitroApp.hooks.callHook('nuxt-security:headers', '/**' ,{
198+
contentSecurityPolicy: {
199+
"script-src": ["'self'", "'unsafe-inline'"],
200+
},
201+
xFrameOptions: false
202+
})
203+
})
204+
})
205+
```

docs/content/1.documentation/2.headers/3.crossOriginEmbedderPolicy.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,11 @@ crossOriginEmbedderPolicy: 'unsafe-none' | 'require-corp' | 'credentialless' | f
6060

6161
### `unsafe-none`
6262

63-
This is the default value. Allows the document to fetch cross-origin resources without giving explicit permission through the CORS protocol or the Cross-Origin-Resource-Policy header.
63+
Allows the document to fetch cross-origin resources without giving explicit permission through the CORS protocol or the Cross-Origin-Resource-Policy header.
6464

6565
### `require-corp`
6666

67-
A document can only load resources from the same origin, or resources explicitly marked as loadable from another origin. If a cross origin resource supports CORS, the crossorigin attribute or the Cross-Origin-Resource-Policy header must be used to load it without being blocked by COEP.
67+
This is the default value. A document can only load resources from the same origin, or resources explicitly marked as loadable from another origin. If a cross origin resource supports CORS, the crossorigin attribute or the Cross-Origin-Resource-Policy header must be used to load it without being blocked by COEP.
6868

6969
### `credentialless`
7070

package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "nuxt-security",
3-
"version": "1.0.1",
3+
"version": "1.1.0",
44
"license": "MIT",
55
"type": "module",
66
"homepage": "https://nuxt-security.vercel.app",
@@ -57,7 +57,7 @@
5757
"defu": "^6.1.1",
5858
"nuxt-csurf": "^1.3.1",
5959
"pathe": "^1.0.0",
60-
"unplugin-remove": "^0.1.6",
60+
"unplugin-remove": "^0.1.7",
6161
"xss": "^1.0.14"
6262
},
6363
"devDependencies": {

playground/nuxt.config.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export default defineNuxtConfig({
2323
rateLimiter: {
2424
tokensPerInterval: 10,
2525
interval: 10000
26-
}
26+
},
27+
runtimeHooks: true
2728
}
2829
})

playground/public/favicon.ico

10.6 KB
Binary file not shown.
+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { defineEventHandler } from "#imports"
2+
3+
export default defineEventHandler((event) => {
4+
return {
5+
csp: getResponseHeader(event, 'Content-Security-Policy')
6+
}
7+
})

playground/server/plugins/headers.ts

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
2+
export default defineNitroPlugin((nitroApp) => {
3+
nitroApp.hooks.hook('nuxt-security:ready', () => {
4+
nitroApp.hooks.callHook('nuxt-security:headers',
5+
{
6+
route: '/api/runtime-hooks',
7+
headers: {
8+
contentSecurityPolicy: {
9+
"script-src": ["'self'", "'unsafe-inline'"],
10+
}
11+
}
12+
})
13+
})
14+
})

src/module.ts

+29-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { fileURLToPath } from 'node:url'
22
import { resolve, normalize } from 'pathe'
3-
import { defineNuxtModule, addServerHandler, installModule, addVitePlugin } from '@nuxt/kit'
3+
import { defineNuxtModule, addServerHandler, installModule, addVitePlugin, addServerPlugin } from '@nuxt/kit'
44
import { defu } from 'defu'
55
import type { Nuxt } from '@nuxt/schema'
66
import viteRemove from 'unplugin-remove/vite'
@@ -126,6 +126,15 @@ export default defineNuxtModule<ModuleOptions>({
126126
}
127127

128128

129+
if(nuxt.options.security.runtimeHooks) {
130+
addServerPlugin(resolve(runtimeDir, 'nitro/plugins/00-context'))
131+
addServerHandler({
132+
handler: normalize(
133+
resolve(runtimeDir, 'server/middleware/headers')
134+
)
135+
})
136+
}
137+
129138
const allowedMethodsRestricterConfig = nuxt.options.security
130139
.allowedMethodsRestricter
131140
if (
@@ -293,6 +302,15 @@ function registerSecurityNitroPlugins(nuxt: Nuxt, securityOptions: ModuleOptions
293302
)
294303
)
295304

305+
// Pre-process HTML into DOM tree
306+
config.plugins.push(
307+
normalize(
308+
fileURLToPath(
309+
new URL('./runtime/nitro/plugins/02a-preprocessHtml', import.meta.url)
310+
)
311+
)
312+
)
313+
296314
// Register nitro plugin to enable Subresource Integrity
297315
config.plugins.push(
298316
normalize(
@@ -331,6 +349,16 @@ function registerSecurityNitroPlugins(nuxt: Nuxt, securityOptions: ModuleOptions
331349
)
332350
)
333351
)
352+
353+
354+
// Recombine HTML from DOM tree
355+
config.plugins.push(
356+
normalize(
357+
fileURLToPath(
358+
new URL('./runtime/nitro/plugins/99b-recombineHtml', import.meta.url)
359+
)
360+
)
361+
)
334362
})
335363

336364
// Make sure our nitro plugins will be applied last
+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { getNameFromKey, headerStringFromObject} from "../../utils/headers"
2+
import { createRouter} from "radix3"
3+
import { defineNitroPlugin } from '#imports'
4+
import { OptionKey } from "~/src/module"
5+
6+
export default defineNitroPlugin((nitroApp) => {
7+
const router = createRouter()
8+
9+
nitroApp.hooks.hook('nuxt-security:headers', ({route, headers: headersConfig}) => {
10+
const headers: Record<string, string |false > = {}
11+
12+
for (const [header, headerOptions] of Object.entries(headersConfig)) {
13+
const headerName = getNameFromKey(header as OptionKey)
14+
if(headerName) {
15+
const value = headerStringFromObject(header as OptionKey, headerOptions)
16+
if(value) {
17+
headers[headerName] = value
18+
} else {
19+
delete headers[headerName]
20+
}
21+
}
22+
}
23+
24+
router.insert(route, headers)
25+
})
26+
27+
nitroApp.hooks.hook('request', (event) => {
28+
event.context.security = event.context.security || {}
29+
event.context.security.headers = router.lookup(event.path)
30+
})
31+
32+
nitroApp.hooks.callHook('nuxt-security:ready')
33+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { defineNitroPlugin, getRouteRules } from '#imports'
2+
import * as cheerio from 'cheerio/lib/slim'
3+
4+
5+
export default defineNitroPlugin((nitroApp) => {
6+
nitroApp.hooks.hook('render:html', async (html, { event }) => {
7+
8+
// Exit if no need to parse HTML for this route
9+
const { security } = getRouteRules(event)
10+
if (!security?.sri && (!security?.headers || !security?.headers.contentSecurityPolicy)) {
11+
return
12+
}
13+
14+
type Section = 'body' | 'bodyAppend' | 'bodyPrepend' | 'head'
15+
const sections = ['body', 'bodyAppend', 'bodyPrepend', 'head'] as Section[]
16+
const cheerios = {} as Record<Section, ReturnType<typeof cheerio.load>[]>
17+
for (const section of sections) {
18+
cheerios[section] = html[section].map(element => {
19+
return cheerio.load(element, {
20+
xml: {
21+
// Disable `xmlMode` to parse HTML with htmlparser2.
22+
xmlMode: false,
23+
},
24+
}, false)
25+
})
26+
}
27+
event.context.cheerios = cheerios
28+
})
29+
})

src/runtime/nitro/plugins/03-subresourceIntegrity.ts

+4-6
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { useStorage, defineNitroPlugin, getRouteRules } from '#imports'
2-
import * as cheerio from 'cheerio'
32
import { isPrerendering } from '../utils'
3+
import { type CheerioAPI } from 'cheerio'
4+
45

56
export default defineNitroPlugin((nitroApp) => {
67
nitroApp.hooks.hook('render:html', async (html, { event }) => {
7-
88
// Exit if SRI not enabled for this route
99
const { security } = getRouteRules(event)
1010
if (!security?.sri) {
@@ -29,10 +29,9 @@ export default defineNitroPlugin((nitroApp) => {
2929
// However the SRI standard provides that other elements may be added to that list in the future
3030
type Section = 'body' | 'bodyAppend' | 'bodyPrepend' | 'head'
3131
const sections = ['body', 'bodyAppend', 'bodyPrepend', 'head'] as Section[]
32+
const cheerios = event.context.cheerios as Record<Section, CheerioAPI[]>
3233
for (const section of sections) {
33-
html[section] = html[section].map(element => {
34-
35-
const $ = cheerio.load(element, null, false)
34+
cheerios[section].forEach($ => {
3635
// Add integrity to all relevant script tags
3736
$('script').each((i, script) => {
3837
const scriptAttrs = $(script).attr()
@@ -68,7 +67,6 @@ export default defineNitroPlugin((nitroApp) => {
6867
}
6968
}
7069
})
71-
return $.html()
7270
})
7371
}
7472
})

src/runtime/nitro/plugins/04-cspSsgHashes.ts

+4-6
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,17 @@ export default defineNitroPlugin((nitroApp) => {
2222
const scriptHashes: Set<string> = new Set()
2323
const styleHashes: Set<string> = new Set()
2424
const hashAlgorithm = 'sha256'
25+
type Section = 'body' | 'bodyAppend' | 'bodyPrepend' | 'head'
26+
const cheerios = event.context.cheerios as Record<Section, ReturnType<typeof cheerio.load>[]>
2527

2628
// Parse HTML if SSG is enabled for this route
2729
if (security.ssg) {
2830
const { hashScripts, hashStyles } = security.ssg
2931

3032
// Scan all relevant sections of the NuxtRenderHtmlContext
31-
type Section = 'body' | 'bodyAppend' | 'bodyPrepend' | 'head'
3233
const sections = ['body', 'bodyAppend', 'bodyPrepend', 'head'] as Section[]
3334
for (const section of sections) {
34-
html[section].forEach(element => {
35-
const $ = cheerio.load(element, null, false)
36-
35+
cheerios[section].forEach($ => {
3736
// Parse all script tags
3837
if (hashScripts) {
3938
$('script').each((i, script) => {
@@ -103,10 +102,9 @@ export default defineNitroPlugin((nitroApp) => {
103102
const csp = security.headers.contentSecurityPolicy
104103
const headerValue = generateCspRules(csp, scriptHashes, styleHashes)
105104
// Insert CSP in the http meta tag
106-
html.head.push(`<meta http-equiv="Content-Security-Policy" content="${headerValue}">`)
105+
cheerios.head.push(cheerio.load(`<meta http-equiv="Content-Security-Policy" content="${headerValue}">`))
107106
// Update rules in HTTP header
108107
setResponseHeader(event, 'Content-Security-Policy', headerValue)
109-
110108
})
111109

112110
// Insert hashes in the CSP meta tag for both the script-src and the style-src policies

0 commit comments

Comments
 (0)