Skip to content

Commit

Permalink
Merge pull request #498 from Shana-AE/fix/regexp-corsHanlder.origin
Browse files Browse the repository at this point in the history
fix: ensure RegExp[] origin can be passed to appSecurityOptions
  • Loading branch information
Baroshem authored Jul 26, 2024
2 parents 2d0ae0a + 4528880 commit 765d7e1
Show file tree
Hide file tree
Showing 3 changed files with 72 additions and 14 deletions.
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;
};

0 comments on commit 765d7e1

Please sign in to comment.