Skip to content

Commit

Permalink
Merge pull request #347 from Baroshem/chore/1.1.0
Browse files Browse the repository at this point in the history
Chore/1.1.0
  • Loading branch information
Baroshem committed Feb 1, 2024
2 parents ef27a76 + e485692 commit 11dc2d5
Show file tree
Hide file tree
Showing 31 changed files with 335 additions and 71 deletions.
2 changes: 1 addition & 1 deletion .stackblitz/package.json
Expand Up @@ -11,6 +11,6 @@
"nuxt": "3.9.3"
},
"dependencies": {
"nuxt-security": "^1.0.1"
"nuxt-security": "^1.1.0"
}
}
38 changes: 13 additions & 25 deletions .stackblitz/yarn.lock
Expand Up @@ -1183,7 +1183,7 @@
dependencies:
"@rollup/pluginutils" "^5.0.2"

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

magic-string@^0.26.3:
version "0.26.7"
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.26.7.tgz#caf7daf61b34e9982f8228c4527474dac8981d6f"
integrity sha512-hX9XH3ziStPoPhJxLq1syWuZMxbDvGNbVchfrdCtanC7D13888bMFow61x8axrx+GfHLtVeAx2kxL7tTGRl+Ow==
dependencies:
sourcemap-codec "^1.4.8"

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:
version "0.30.5"
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.5.tgz#1994d980bd1c8835dc6e78db7cbd4ae4f24746f9"
Expand Down Expand Up @@ -4108,18 +4101,18 @@ nuxt-csurf@^1.3.1:
defu "^6.1.1"
uncsrf "^1.1.1"

nuxt-security@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/nuxt-security/-/nuxt-security-1.0.1.tgz#a98a5b8636e46c6ff97bc7ec104d03b4c58aee0f"
integrity sha512-GT504TFIJn0sM6yhdiUIf5EXoakYir0zs/2KO4uLqcP6XyX7FDj/SW0D3r8EvK80peL2dPvcWRmYRaqo1vPU2g==
nuxt-security@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/nuxt-security/-/nuxt-security-1.1.0.tgz#ccb5600cbb835a4523fe22f89b7fd23a6544a287"
integrity sha512-nsvdUQbHjpGPNRUYi+ZDB5yUGL7rjTIhTxpuZwqxJuOPwAbw19S2DlgE+HTxn9CmzTCv8231lEhpD5G/Y4uG7g==
dependencies:
"@nuxt/kit" "^3.8.0"
basic-auth "^2.0.1"
cheerio "^1.0.0-rc.12"
defu "^6.1.1"
nuxt-csurf "^1.3.1"
pathe "^1.0.0"
unplugin-remove "^0.1.6"
unplugin-remove "^0.1.7"
xss "^1.0.14"

nuxt@3.9.3:
Expand Down Expand Up @@ -5101,11 +5094,6 @@ source-map@^0.7.4:
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.4.tgz#a9bbe705c9d8846f4e08ff6765acf0f1b0898656"
integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==

sourcemap-codec@^1.4.8:
version "1.4.8"
resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4"
integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==

spdx-correct@^3.0.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.2.0.tgz#4f5ab0668f0059e34f9c00dce331784a12de4e9c"
Expand Down Expand Up @@ -5517,14 +5505,14 @@ universalify@^2.0.0:
resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717"
integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==

unplugin-remove@^0.1.6:
version "0.1.6"
resolved "https://registry.yarnpkg.com/unplugin-remove/-/unplugin-remove-0.1.6.tgz#0b3d0a77ef2061de8a85cc239a5ba7f5c64d535d"
integrity sha512-/jwD4+ZzeBGC32Rx7m59FOhqALmtLsTeabFwaYM8yQMVaVO8un8AQxZi3YFJirPzJgEW43e5/wQpze8z/WwOxA==
unplugin-remove@^0.1.7:
version "0.1.7"
resolved "https://registry.yarnpkg.com/unplugin-remove/-/unplugin-remove-0.1.7.tgz#0ee1b14963a6c186f5f263224b7085bf260913a5"
integrity sha512-7BaEfgR/hMQRgaN++RAILeq9/wBrJPqCLRsQH+ow8979s2TE3TAFE4rQRmMUPcJ/w4ccsVSLepwGbimLMkQjyA==
dependencies:
"@rollup/pluginutils" "^4.2.1"
magic-string "^0.26.3"
unplugin "^1.5.0"
"@rollup/pluginutils" "^5.1.0"
magic-string "^0.30.5"
unplugin "^1.5.1"

unplugin-vue-router@^0.7.0:
version "0.7.0"
Expand Down
2 changes: 1 addition & 1 deletion docs/content/0.index.md
Expand Up @@ -92,7 +92,7 @@ cta:
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.

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


Expand Down
Expand Up @@ -98,7 +98,6 @@ security: {
preflight: {
statusCode: 204
},
throwError: true
},
allowedMethodsRestricter: {
methods: '*',
Expand Down
34 changes: 34 additions & 0 deletions docs/content/1.documentation/1.getting-started/3.usage.md
Expand Up @@ -169,3 +169,37 @@ export default defineNuxtConfig({
}
})
```

## Runtime configuration

If you need to change the headers configuration at runtime, it is possible to do it through `nuxt-security:headers` hook.

### Enabling the option

This feature is optional, you can enable it with

```ts
export default defineNuxtConfig({
modules: ['nuxt-security'],
security: {
runtimeHooks: true
}
})
```

### Usage

Within your nitro plugin. You can override the previous configuration of a route with `nuxt-security:headers`.

```ts
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('nuxt-security:ready', () => {
nitroApp.hooks.callHook('nuxt-security:headers', '/**' ,{
contentSecurityPolicy: {
"script-src": ["'self'", "'unsafe-inline'"],
},
xFrameOptions: false
})
})
})
```
Expand Up @@ -60,11 +60,11 @@ crossOriginEmbedderPolicy: 'unsafe-none' | 'require-corp' | 'credentialless' | f

### `unsafe-none`

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.
Allows the document to fetch cross-origin resources without giving explicit permission through the CORS protocol or the Cross-Origin-Resource-Policy header.

### `require-corp`

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.
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.

### `credentialless`

Expand Down
4 changes: 2 additions & 2 deletions package.json
@@ -1,6 +1,6 @@
{
"name": "nuxt-security",
"version": "1.0.1",
"version": "1.1.0",
"license": "MIT",
"type": "module",
"homepage": "https://nuxt-security.vercel.app",
Expand Down Expand Up @@ -57,7 +57,7 @@
"defu": "^6.1.1",
"nuxt-csurf": "^1.3.1",
"pathe": "^1.0.0",
"unplugin-remove": "^0.1.6",
"unplugin-remove": "^0.1.7",
"xss": "^1.0.14"
},
"devDependencies": {
Expand Down
3 changes: 2 additions & 1 deletion playground/nuxt.config.ts
Expand Up @@ -23,6 +23,7 @@ export default defineNuxtConfig({
rateLimiter: {
tokensPerInterval: 10,
interval: 10000
}
},
runtimeHooks: true
}
})
Binary file added playground/public/favicon.ico
Binary file not shown.
7 changes: 7 additions & 0 deletions playground/server/api/runtime-hooks.ts
@@ -0,0 +1,7 @@
import { defineEventHandler } from "#imports"

export default defineEventHandler((event) => {
return {
csp: getResponseHeader(event, 'Content-Security-Policy')
}
})
14 changes: 14 additions & 0 deletions playground/server/plugins/headers.ts
@@ -0,0 +1,14 @@

export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('nuxt-security:ready', () => {
nitroApp.hooks.callHook('nuxt-security:headers',
{
route: '/api/runtime-hooks',
headers: {
contentSecurityPolicy: {
"script-src": ["'self'", "'unsafe-inline'"],
}
}
})
})
})
30 changes: 29 additions & 1 deletion src/module.ts
@@ -1,6 +1,6 @@
import { fileURLToPath } from 'node:url'
import { resolve, normalize } from 'pathe'
import { defineNuxtModule, addServerHandler, installModule, addVitePlugin } from '@nuxt/kit'
import { defineNuxtModule, addServerHandler, installModule, addVitePlugin, addServerPlugin } from '@nuxt/kit'
import { defu } from 'defu'
import type { Nuxt } from '@nuxt/schema'
import viteRemove from 'unplugin-remove/vite'
Expand Down Expand Up @@ -126,6 +126,15 @@ export default defineNuxtModule<ModuleOptions>({
}


if(nuxt.options.security.runtimeHooks) {
addServerPlugin(resolve(runtimeDir, 'nitro/plugins/00-context'))
addServerHandler({
handler: normalize(
resolve(runtimeDir, 'server/middleware/headers')
)
})
}

const allowedMethodsRestricterConfig = nuxt.options.security
.allowedMethodsRestricter
if (
Expand Down Expand Up @@ -293,6 +302,15 @@ function registerSecurityNitroPlugins(nuxt: Nuxt, securityOptions: ModuleOptions
)
)

// Pre-process HTML into DOM tree
config.plugins.push(
normalize(
fileURLToPath(
new URL('./runtime/nitro/plugins/02a-preprocessHtml', import.meta.url)
)
)
)

// Register nitro plugin to enable Subresource Integrity
config.plugins.push(
normalize(
Expand Down Expand Up @@ -331,6 +349,16 @@ function registerSecurityNitroPlugins(nuxt: Nuxt, securityOptions: ModuleOptions
)
)
)


// Recombine HTML from DOM tree
config.plugins.push(
normalize(
fileURLToPath(
new URL('./runtime/nitro/plugins/99b-recombineHtml', import.meta.url)
)
)
)
})

// Make sure our nitro plugins will be applied last
Expand Down
33 changes: 33 additions & 0 deletions src/runtime/nitro/plugins/00-context.ts
@@ -0,0 +1,33 @@
import { getNameFromKey, headerStringFromObject} from "../../utils/headers"
import { createRouter} from "radix3"
import { defineNitroPlugin } from '#imports'
import { OptionKey } from "~/src/module"

export default defineNitroPlugin((nitroApp) => {
const router = createRouter()

nitroApp.hooks.hook('nuxt-security:headers', ({route, headers: headersConfig}) => {
const headers: Record<string, string |false > = {}

for (const [header, headerOptions] of Object.entries(headersConfig)) {
const headerName = getNameFromKey(header as OptionKey)
if(headerName) {
const value = headerStringFromObject(header as OptionKey, headerOptions)
if(value) {
headers[headerName] = value
} else {
delete headers[headerName]
}
}
}

router.insert(route, headers)
})

nitroApp.hooks.hook('request', (event) => {
event.context.security = event.context.security || {}
event.context.security.headers = router.lookup(event.path)
})

nitroApp.hooks.callHook('nuxt-security:ready')
})
29 changes: 29 additions & 0 deletions src/runtime/nitro/plugins/02a-preprocessHtml.ts
@@ -0,0 +1,29 @@
import { defineNitroPlugin, getRouteRules } from '#imports'
import * as cheerio from 'cheerio/lib/slim'


export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('render:html', async (html, { event }) => {

// Exit if no need to parse HTML for this route
const { security } = getRouteRules(event)
if (!security?.sri && (!security?.headers || !security?.headers.contentSecurityPolicy)) {
return
}

type Section = 'body' | 'bodyAppend' | 'bodyPrepend' | 'head'
const sections = ['body', 'bodyAppend', 'bodyPrepend', 'head'] as Section[]
const cheerios = {} as Record<Section, ReturnType<typeof cheerio.load>[]>
for (const section of sections) {
cheerios[section] = html[section].map(element => {
return cheerio.load(element, {
xml: {
// Disable `xmlMode` to parse HTML with htmlparser2.
xmlMode: false,
},
}, false)
})
}
event.context.cheerios = cheerios
})
})
10 changes: 4 additions & 6 deletions src/runtime/nitro/plugins/03-subresourceIntegrity.ts
@@ -1,10 +1,10 @@
import { useStorage, defineNitroPlugin, getRouteRules } from '#imports'
import * as cheerio from 'cheerio'
import { isPrerendering } from '../utils'
import { type CheerioAPI } from 'cheerio'


export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('render:html', async (html, { event }) => {

// Exit if SRI not enabled for this route
const { security } = getRouteRules(event)
if (!security?.sri) {
Expand All @@ -29,10 +29,9 @@ export default defineNitroPlugin((nitroApp) => {
// However the SRI standard provides that other elements may be added to that list in the future
type Section = 'body' | 'bodyAppend' | 'bodyPrepend' | 'head'
const sections = ['body', 'bodyAppend', 'bodyPrepend', 'head'] as Section[]
const cheerios = event.context.cheerios as Record<Section, CheerioAPI[]>
for (const section of sections) {
html[section] = html[section].map(element => {

const $ = cheerio.load(element, null, false)
cheerios[section].forEach($ => {
// Add integrity to all relevant script tags
$('script').each((i, script) => {
const scriptAttrs = $(script).attr()
Expand Down Expand Up @@ -68,7 +67,6 @@ export default defineNitroPlugin((nitroApp) => {
}
}
})
return $.html()
})
}
})
Expand Down
10 changes: 4 additions & 6 deletions src/runtime/nitro/plugins/04-cspSsgHashes.ts
Expand Up @@ -22,18 +22,17 @@ export default defineNitroPlugin((nitroApp) => {
const scriptHashes: Set<string> = new Set()
const styleHashes: Set<string> = new Set()
const hashAlgorithm = 'sha256'
type Section = 'body' | 'bodyAppend' | 'bodyPrepend' | 'head'
const cheerios = event.context.cheerios as Record<Section, ReturnType<typeof cheerio.load>[]>

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

// Scan all relevant sections of the NuxtRenderHtmlContext
type Section = 'body' | 'bodyAppend' | 'bodyPrepend' | 'head'
const sections = ['body', 'bodyAppend', 'bodyPrepend', 'head'] as Section[]
for (const section of sections) {
html[section].forEach(element => {
const $ = cheerio.load(element, null, false)

cheerios[section].forEach($ => {
// Parse all script tags
if (hashScripts) {
$('script').each((i, script) => {
Expand Down Expand Up @@ -103,10 +102,9 @@ export default defineNitroPlugin((nitroApp) => {
const csp = security.headers.contentSecurityPolicy
const headerValue = generateCspRules(csp, scriptHashes, styleHashes)
// Insert CSP in the http meta tag
html.head.push(`<meta http-equiv="Content-Security-Policy" content="${headerValue}">`)
cheerios.head.push(cheerio.load(`<meta http-equiv="Content-Security-Policy" content="${headerValue}">`))
// Update rules in HTTP header
setResponseHeader(event, 'Content-Security-Policy', headerValue)

})

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

0 comments on commit 11dc2d5

Please sign in to comment.