Skip to content
New issue

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

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

Already on GitHub? # to your account

fix: ensure RegExp[] origin can be passed to appSecurityOptions #498

Merged
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
29 changes: 18 additions & 11 deletions src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { defaultSecurityConfig } from './defaultConfig'
import type { Nuxt } from '@nuxt/schema'
import type { Nitro } from 'nitropack'
import type { ModuleOptions } from './types/module'
import { modifyCorsHandlerOriginRegExpToJSON } from './utils/originRegExpSerde'

export * from './types/module'
export * from './types/headers'
Expand All @@ -23,7 +24,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 @@ -55,6 +56,9 @@ export default defineNuxtModule<ModuleOptions>({
// At this point we have all security options merged into runtimeConfig
const securityOptions = nuxt.options.runtimeConfig.security

// Avoid JSON.stringify regexp to '{}'
modifyCorsHandlerOriginRegExpToJSON(securityOptions.corsHandler)

// Disable module when `enabled` is set to `false`
if (!securityOptions.enabled) { return }

Expand All @@ -75,6 +79,9 @@ 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.corsHandler) {
modifyCorsHandlerOriginRegExpToJSON(rule.security.corsHandler)
}
if (rule.security && rule.security.headers) {
const { security : { headers } } = rule
const routeSecurityHeaders = getHeadersApplicableToAllResources(headers)
Expand All @@ -84,7 +91,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 @@ -135,12 +142,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 @@ -174,12 +181,12 @@ export default defineNuxtModule<ModuleOptions>({
})

// Register init hook to add pre-rendered headers to responses
nuxt.hook('nitro:init', nitro => {
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 @@ -203,7 +210,7 @@ export default defineNuxtModule<ModuleOptions>({
})

/**
*
*
* Register storage driver for the rate limiter
*/
function registerRateLimiterStorage(nuxt: Nuxt, securityOptions: ModuleOptions) {
Expand All @@ -229,8 +236,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
16 changes: 13 additions & 3 deletions src/runtime/nitro/plugins/00-routeRules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { defineNitroPlugin, useRuntimeConfig } from "#imports"
import { getAppSecurityOptions } from '../context'
import { defuReplaceArray } from '../../../utils/merge'
import { standardToSecurity, backwardsCompatibleSecurity } from '../../../utils/headers'
import { getRegExpOriginRestoredCorsHandler } from '../../../utils/originRegExpSerde';

/**
* This plugin merges all security options into the global security context
Expand All @@ -24,9 +25,15 @@ export default defineNitroPlugin(async(nitroApp) => {
const securityOptions = runtimeConfig.security
const { headers } = securityOptions

// Restore origin regexp
const corsHandlerOption = getRegExpOriginRestoredCorsHandler(securityOptions.corsHandler)

const securityHeaders = backwardsCompatibleSecurity(headers)
appSecurityOptions['/**'] = defuReplaceArray(
{ headers: securityHeaders },
{
headers: securityHeaders,
corsHandler: corsHandlerOption,
},
securityOptions,
appSecurityOptions['/**']
)
Expand All @@ -40,8 +47,12 @@ export default defineNitroPlugin(async(nitroApp) => {
if (security) {
const { headers } = security
const securityHeaders = backwardsCompatibleSecurity(headers)
const corsHandlerOption = getRegExpOriginRestoredCorsHandler(security.corsHandler)
appSecurityOptions[route] = defuReplaceArray(
{ headers: securityHeaders },
{
headers: securityHeaders,
corsHandler: corsHandlerOption
},
security,
appSecurityOptions[route],
)
Expand All @@ -63,4 +74,3 @@ export default defineNitroPlugin(async(nitroApp) => {

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

41 changes: 41 additions & 0 deletions src/utils/originRegExpSerde.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import type { CorsOptions } from '../module';

const REG_SERIALIZE_KEY = '_nuxt_security_origin_serialized_regexp';

export const modifyRegExpToJSON = (reg: RegExp) => {
(reg as any).toJSON = () => ({
[REG_SERIALIZE_KEY]: reg.toString(),
});
};

export const getDeserializedRegExp = (regObj: Record<string, string>) => {
const regStr = regObj?.[REG_SERIALIZE_KEY];
if (typeof regStr !== 'string') return null;
const fragments = regStr.match(/\/(.*?)\/([a-z]*)?$/i);
if (!fragments?.length) return null;
return new RegExp(fragments?.[1], fragments?.[2] || '');
};

export const modifyCorsHandlerOriginRegExpToJSON = (corsHandler?: CorsOptions | false) => {
if (typeof corsHandler === 'object' && Array.isArray(corsHandler.origin)) {
corsHandler.origin.forEach((o) => {
o instanceof RegExp && modifyRegExpToJSON(o);
});
}
};

export const getRegExpOriginRestoredCorsHandler = (corsHandler?: CorsOptions | false) => {
let originOption = typeof corsHandler === 'object' ? corsHandler.origin : undefined;
if (typeof corsHandler !== 'object') return corsHandler;
if (Array.isArray(corsHandler.origin)) {
originOption = corsHandler.origin.map((o) => {
const origin = getDeserializedRegExp(o as any);
return origin ?? o;
});
}
const result: CorsOptions = {
...corsHandler,
origin: originOption,
};
return result;
};
Loading