Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
28 changes: 14 additions & 14 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,26 +52,26 @@
},
"packageManager": "yarn@1.22.19",
"dependencies": {
"@nuxt/kit": "^3.11.2",
"@nuxt/kit": "^4.2.1",
"basic-auth": "^2.0.1",
"defu": "^6.1.1",
"defu": "^6.1.4",
"nuxt-csurf": "^1.6.5",
"pathe": "^1.0.0",
"pathe": "^2.0.3",
"unplugin-remove": "^1.0.3",
"xss": "^1.0.14"
"xss": "^1.0.15"
},
"devDependencies": {
"@nuxt/eslint-config": "^0.3.10",
"@nuxt/module-builder": "^1.0.1",
"@nuxt/schema": "^3.11.2",
"@nuxt/test-utils": "^3.12.0",
"@types/node": "^20.14.8",
"changelogen": "^0.5.7",
"eslint": "^8.50.0",
"nuxi": "^3.26.4",
"nuxt": "^3.11.2",
"typescript": "^5.4.5",
"vitest": "^1.3.1"
"@nuxt/module-builder": "^1.0.2",
"@nuxt/schema": "^4.2.1",
"@nuxt/test-utils": "^3.20.1",
"@types/node": "^24.10.1",
"changelogen": "^0.6.2",
"eslint": "^8.57.0",
"nuxi": "^3.30.0",
"nuxt": "^4.2.1",
"typescript": "^5.9.3",
"vitest": "^4.0.8"
},
"stackblitz": {
"installDependencies": false,
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
3 changes: 0 additions & 3 deletions playground/server/tsconfig.json

This file was deleted.

10 changes: 8 additions & 2 deletions playground/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
{
"extends": "./.nuxt/tsconfig.json"
}
"files": [],
"references": [
{ "path": "./.nuxt/tsconfig.app.json" },
{ "path": "./.nuxt/tsconfig.server.json" },
{ "path": "./.nuxt/tsconfig.shared.json" },
{ "path": "./.nuxt/tsconfig.node.json" }
]
}
24 changes: 12 additions & 12 deletions src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export default defineNuxtModule<ModuleOptions>({
},
async setup (options, nuxt) {
const resolver = createResolver(import.meta.url)

nuxt.options.build.transpile.push(resolver.resolve('./runtime'))

// First merge module options with default options
Expand Down Expand Up @@ -79,7 +79,7 @@ export default defineNuxtModule<ModuleOptions>({
} else {
// In case of esbuild, set the drop option
nuxt.options.vite.esbuild = defu(
{
{
drop: ['console', 'debugger'] as ('console' | 'debugger')[],
},
nuxt.options.vite.esbuild
Expand All @@ -100,7 +100,7 @@ export default defineNuxtModule<ModuleOptions>({
// Then insert route specific security headers
for (const route in nuxt.options.nitro.routeRules) {
const rule = nuxt.options.nitro.routeRules[route]
if (rule.security && rule.security.headers) {
if (rule && rule.security && rule.security.headers) {
const { security : { headers } } = rule
const routeSecurityHeaders = getHeadersApplicableToAllResources(headers)
nuxt.options.nitro.routeRules[route] = defuReplaceArray(
Expand All @@ -109,7 +109,7 @@ export default defineNuxtModule<ModuleOptions>({
)
}
}

// Register nitro plugin to manage security rules at the level of each route
addServerPlugin(resolver.resolve('./runtime/nitro/plugins/00-routeRules'))

Expand Down Expand Up @@ -160,12 +160,12 @@ export default defineNuxtModule<ModuleOptions>({
addServerHandler({
handler: resolver.resolve('./runtime/server/middleware/rateLimiter')
})

// Register XSS validator middleware
addServerHandler({
handler: resolver.resolve('./runtime/server/middleware/xssValidator')
})

// Register basicAuth middleware that is disabled by default
const basicAuthConfig = nuxt.options.runtimeConfig.private.basicAuth
if (basicAuthConfig && (basicAuthConfig.enabled || (basicAuthConfig as any)?.value?.enabled)) {
Expand Down Expand Up @@ -291,9 +291,9 @@ export {}
nuxt.hook('nitro:init', nitro => {
nitro.hooks.hook('prerender:done', async() => {
// Add the prenredered headers to the Nitro server assets
nitro.options.serverAssets.push({
baseName: 'nuxt-security',
dir: createResolver(nuxt.options.buildDir).resolve('./nuxt-security')
nitro.options.serverAssets.push({
baseName: 'nuxt-security',
dir: createResolver(nuxt.options.buildDir).resolve('./nuxt-security')
})

// In some Nitro presets (e.g. Vercel), the header rules are generated for the static server
Expand All @@ -317,7 +317,7 @@ export {}
})

/**
*
*
* Register storage driver for the rate limiter
*/
function registerRateLimiterStorage(nuxt: Nuxt, securityOptions: ModuleOptions) {
Expand All @@ -343,8 +343,8 @@ function registerRateLimiterStorage(nuxt: Nuxt, securityOptions: ModuleOptions)
* Make sure our nitro plugins will be applied last,
* After all other third-party modules that might have loaded their own nitro plugins
*/
function reorderNitroPlugins(nuxt: Nuxt) {
nuxt.hook('nitro:init', nitro => {
function reorderNitroPlugins(nuxt: Nuxt) {
nuxt.hook('nitro:init', nitro => {
const resolver = createResolver(import.meta.url)
const securityPluginsPrefix = resolver.resolve('./runtime/nitro/plugins')

Expand Down
11 changes: 5 additions & 6 deletions src/runtime/nitro/context/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,16 @@ export function resolveSecurityRules(event: H3Event): NuxtSecurityRouteRules {
if (!event.context.security.rules) {
const router = createRouter<NuxtSecurityRouteRules>({ routes: structuredClone(nitroAppSecurityOptions) })
const matcher = toRouteMatcher(router)
const matches = matcher.matchAll(event.path.split('?')[0])
const eventPathNoQuery = event.path.split('?')[0]
const matches = eventPathNoQuery ? matcher.matchAll(eventPathNoQuery) : []
const rules: NuxtSecurityRouteRules = defuReplaceArray({}, ...matches.reverse())
event.context.security.rules = rules
}
return event.context.security.rules
}

/**
* Returns the security route that was matched for a specific request
* Returns the security route that was matched for a specific request
*/
export function resolveSecurityRoute(event: H3Event) {
if (!event.context.security) {
Expand All @@ -40,12 +41,10 @@ export function resolveSecurityRoute(event: H3Event) {
if (!event.context.security.route) {
const routeNames = Object.fromEntries(Object.entries(nitroAppSecurityOptions).map(([name]) => [name, { name }]))
const router = createRouter<{ name: string }>({ routes: routeNames})
const match = router.lookup(event.path.split('?')[0])
const eventPathNoQuery = event.path.split('?')[0]
const match = eventPathNoQuery ? router.lookup(eventPathNoQuery) : undefined
const route = match?.name ?? ''
event.context.security.route = route
}
return event.context.security.route
}



5 changes: 3 additions & 2 deletions src/runtime/nitro/plugins/00-routeRules.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { defineNitroPlugin, useRuntimeConfig } from "#imports"
import { defineNitroPlugin, useRuntimeConfig } from "nitropack/runtime"
import { getAppSecurityOptions } from '../context'
import { defuReplaceArray } from '../../../utils/merge'
import { standardToSecurity, backwardsCompatibleSecurity } from '../../../utils/headers'
Expand All @@ -13,6 +13,7 @@ export default defineNitroPlugin(async(nitroApp) => {
// First insert standard route rules headers
for (const route in runtimeConfig.nitro.routeRules) {
const rule = runtimeConfig.nitro.routeRules[route]
if (!rule) continue
const { headers } = rule
const securityHeaders = standardToSecurity(headers)
if (securityHeaders) {
Expand All @@ -36,6 +37,7 @@ export default defineNitroPlugin(async(nitroApp) => {
// Then insert route specific security headers
for (const route in runtimeConfig.nitro.routeRules) {
const rule = runtimeConfig.nitro.routeRules[route]
if (!rule) continue
const { security } = rule
if (security) {
const { headers } = security
Expand Down Expand Up @@ -63,4 +65,3 @@ export default defineNitroPlugin(async(nitroApp) => {

await nitroApp.hooks.callHook('nuxt-security:ready')
})

4 changes: 2 additions & 2 deletions src/runtime/nitro/plugins/20-subresourceIntegrity.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { defineNitroPlugin } from '#imports'
import { defineNitroPlugin } from 'nitropack/runtime'
//@ts-expect-error : we are importing from the virtual file system
import sriHashes from '#sri-hashes'
import { resolveSecurityRules } from '../context'
Expand Down Expand Up @@ -28,7 +28,7 @@ export default defineNitroPlugin((nitroApp) => {
if (typeof element !== 'string') {
return element;
}

element = element.replace(SCRIPT_RE, (match, rest: string, src: string) => {
const hash = sriHashes[src]
if (hash) {
Expand Down
6 changes: 4 additions & 2 deletions src/runtime/nitro/plugins/30-cspSsgHashes.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { defineNitroPlugin } from '#imports'
import { defineNitroPlugin } from 'nitropack/runtime'
import { resolveSecurityRules } from '../context'
import { generateHash } from '../../../utils/crypto'
import type { Section } from '../../../types/module'
Expand Down Expand Up @@ -48,6 +48,7 @@ export default defineNitroPlugin((nitroApp) => {
// Parse all script tags
const inlineScriptMatches = element.matchAll(INLINE_SCRIPT_RE)
for (const [, scriptText] of inlineScriptMatches) {
if (!scriptText) continue
const hash = await generateHash(scriptText, hashAlgorithm)
scriptHashes.add(`'${hash}'`)
}
Expand All @@ -61,6 +62,7 @@ export default defineNitroPlugin((nitroApp) => {
if (hashStyles) {
const styleMatches = element.matchAll(STYLE_RE)
for (const [, styleText] of styleMatches) {
if (!styleText) continue
const hash = await generateHash(styleText, hashAlgorithm)
styleHashes.add(`'${hash}'`)
}
Expand Down Expand Up @@ -100,4 +102,4 @@ export default defineNitroPlugin((nitroApp) => {
}
}
})
})
})
2 changes: 1 addition & 1 deletion src/runtime/nitro/plugins/40-cspSsrNonce.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { defineNitroPlugin } from '#imports'
import { defineNitroPlugin } from 'nitropack/runtime'
import { resolveSecurityRules } from '../context'
import { generateRandomNonce } from '../../../utils/crypto'

Expand Down
9 changes: 4 additions & 5 deletions src/runtime/nitro/plugins/50-updateCsp.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { defineNitroPlugin } from '#imports'
import { defineNitroPlugin } from 'nitropack/runtime'
import { resolveSecurityRules } from '../context'
import type { ContentSecurityPolicyValue } from '../../../types/headers'

Expand All @@ -7,7 +7,7 @@ import type { ContentSecurityPolicyValue } from '../../../types/headers'
*/
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('render:html', (response, { event }) => {
// TODO: find alternative for modern Nuxt versions that don't have the island property anymore, or remove logic
// @ts-expect-error TODO: find alternative for modern Nuxt versions that don't have the island property anymore, or remove logic
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.
Expand All @@ -17,7 +17,7 @@ export default defineNitroPlugin((nitroApp) => {
const rules = resolveSecurityRules(event)
if (rules.enabled && rules.headers) {
const headers = rules.headers

if (headers.contentSecurityPolicy) {
const csp = headers.contentSecurityPolicy
const nonce = event.context.security?.nonce
Expand Down Expand Up @@ -53,7 +53,7 @@ function updateCspVariables(csp: ContentSecurityPolicyValue, nonce?: string, scr
}
})
.filter(source => source)

if (['script-src', 'script-src-elem'].includes(directive) && scriptHashes) {
modifiedSources.push(...scriptHashes)
}
Expand All @@ -65,4 +65,3 @@ function updateCspVariables(csp: ContentSecurityPolicyValue, nonce?: string, scr
}))
return generatedCsp
}

10 changes: 5 additions & 5 deletions src/runtime/nitro/plugins/60-recombineHtml.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { defineNitroPlugin } from '#imports'
import { defineNitroPlugin } from 'nitropack/runtime'
import { resolveSecurityRules } from '../context'
import { headerStringFromObject } from '../../../utils/headers'

Expand All @@ -25,12 +25,12 @@ 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
if (html.head.length > 0) {
const metaCharsetMatch = html.head[0].match(/^<meta charset="(.*?)">/mdi)
if (metaCharsetMatch && metaCharsetMatch.indices) {
const metaCharsetMatch = html.head[0]?.match(/^<meta charset="(.*?)">/mdi)
if (metaCharsetMatch?.indices && metaCharsetMatch.indices[0] && metaCharsetMatch.indices[0][1]) {
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)
}
}
})
})
})
3 changes: 2 additions & 1 deletion src/runtime/nitro/plugins/70-securityHeaders.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { defineNitroPlugin, setResponseHeader, removeResponseHeader, getRouteRules, getResponseHeader } from '#imports'
import { defineNitroPlugin, getRouteRules } from 'nitropack/runtime'
import { setResponseHeader, removeResponseHeader, getResponseHeader } from 'h3'
import { resolveSecurityRules } from '../context'
import { getNameFromKey, headerStringFromObject } from '../../../utils/headers'
import type { OptionKey } from '../../../types/headers'
Expand Down
3 changes: 2 additions & 1 deletion src/runtime/nitro/plugins/80-hidePoweredBy.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { defineNitroPlugin, removeResponseHeader } from '#imports'
import { defineNitroPlugin } from 'nitropack/runtime'
import { removeResponseHeader } from 'h3'
import { resolveSecurityRules } from '../context'

/**
Expand Down
10 changes: 6 additions & 4 deletions src/runtime/nitro/plugins/90-prerenderedHeaders.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { defineNitroPlugin, getResponseHeaders, setResponseHeaders, useStorage } from '#imports'
import { OutgoingHttpHeaders } from 'http'
import { defineNitroPlugin, useStorage } from 'nitropack/runtime'
import { getResponseHeaders, setResponseHeaders } from 'h3'
import type { OutgoingHttpHeaders } from 'http'
import { resolveSecurityRules } from '../context'

/**
Expand All @@ -15,6 +16,7 @@ export default defineNitroPlugin(async(nitroApp) => {
// We save the headers for the current path
const headers = getResponseHeaders(event)
const path = event.path.split('?')[0]
if (!path) return
// This is a hack
// It works because headers is an object
// 70-securityHeaders is executed after this step
Expand Down Expand Up @@ -53,10 +55,10 @@ export default defineNitroPlugin(async(nitroApp) => {
if (rules.enabled && rules.ssg && rules.ssg.nitroHeaders) {
const path = event.path.split('?')[0]
// We retrieve the headers for the current path
if (prerenderedHeaders[path]) {
if (path && prerenderedHeaders[path]) {
setResponseHeaders(event, prerenderedHeaders[path])
}
}
})
}
})
})
2 changes: 1 addition & 1 deletion src/runtime/server/middleware/allowedMethodsRestricter.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { defineEventHandler, createError } from '#imports'
import { defineEventHandler, createError } from 'h3'
import { resolveSecurityRules } from '../../nitro/context'
import type { HTTPMethod } from '../../../types/middlewares'

Expand Down
3 changes: 2 additions & 1 deletion src/runtime/server/middleware/basicAuth.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useRuntimeConfig, createError, defineEventHandler, sendError, setHeader } from '#imports'
import { useRuntimeConfig } from 'nitropack/runtime'
import { createError, defineEventHandler, sendError, setHeader } from 'h3'
// @ts-ignore : the basic-auth module does not export types
import getCredentials from 'basic-auth'

Expand Down
2 changes: 1 addition & 1 deletion src/runtime/server/middleware/corsHandler.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { defineEventHandler, handleCors } from '#imports'
import { defineEventHandler, handleCors } from 'h3'
import type { H3CorsOptions } from 'h3'
import { resolveSecurityRules } from '../../nitro/context'

Expand Down
Loading