Skip to content

Commit

Permalink
feat: ✨ per-route configuration & ability to disable
Browse files Browse the repository at this point in the history
Resolves #25, resolves Baroshem/nuxt-security#334

Drop `excludedUrls` option in favor of `routeRules`
  • Loading branch information
Morgbn committed Mar 17, 2024
1 parent a88f882 commit 7550de1
Show file tree
Hide file tree
Showing 6 changed files with 40 additions and 19 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Create a middleware for CSRF token creation and validation.

✅ Supports Node.js server & serverless environments \
✅ Supports both universal and client-side rendering (`ssr: true|false`) \
✅ Per-route configuration \
✅ TypeScript

## Setup
Expand Down Expand Up @@ -39,7 +40,6 @@ The only thing you need to do to use the module in the default configuration is
sameSite: 'strict'
},
methodsToProtect: ['POST', 'PUT', 'PATCH'], // the request methods we want CSRF protection for
excludedUrls: ['/nocsrf1', ['/nocsrf2/.*', 'i']], // any URLs we want to exclude from CSRF protection
encryptSecret: /** a 32 bits secret */, // only for non serverless runtime, random bytes by default
encryptAlgorithm: 'aes-256-cbc', // by default 'aes-256-cbc' (node), 'AES-CBC' (serverless)
addCsrfTokenToEventCtx: true // default false, to run useCsrfFetch on server set it to true
Expand Down
7 changes: 6 additions & 1 deletion playground/nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,13 @@ export default defineNuxtConfig({
modules: [
module
],
routeRules: {
'/api/nocsrf': {
csurf: false
}
},
csurf: {
https: false,
excludedUrls: [['/no.*', 'i'], '/test-without-csrf']
methodsToProtect: ['POST']
}
})
21 changes: 14 additions & 7 deletions src/module.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { defu } from 'defu'
import { defineNuxtModule, createResolver, addServerHandler, addServerPlugin, addImports, addPlugin } from '@nuxt/kit'

import { defuReplaceArray } from './utils'
import type { ModuleOptions } from './types'

export * from './types'
Expand All @@ -18,8 +17,7 @@ export default defineNuxtModule<ModuleOptions>({
httpOnly: true,
sameSite: 'strict'
},
methodsToProtect: ['POST', 'PUT', 'PATCH'],
excludedUrls: []
methodsToProtect: ['POST', 'PUT', 'PATCH']
},
setup (options, nuxt) {
const { resolve } = createResolver(import.meta.url)
Expand All @@ -32,9 +30,12 @@ export default defineNuxtModule<ModuleOptions>({
options.cookie.secure = !!options.https
}

nuxt.options.runtimeConfig.csurf = defu(nuxt.options.runtimeConfig.csurf, { ...options })
addServerHandler({ handler: resolve('runtime/server/middleware/csrf') })
addServerPlugin(resolve('runtime/server/plugin/csrf'))
nuxt.options.runtimeConfig.csurf = defuReplaceArray(nuxt.options.runtimeConfig.csurf, { ...options })

if (options.enabled !== false) {
addServerHandler({ handler: resolve('runtime/server/middleware/csrf') })
addServerPlugin(resolve('runtime/server/plugin/csrf'))
}

// Transpile runtime
nuxt.options.build.transpile.push(resolve('runtime'))
Expand All @@ -54,3 +55,9 @@ declare module 'nuxt/schema' {
csurf: ModuleOptions
}
}

declare module 'nitropack' {
interface NitroRouteConfig {
csurf?: Partial<ModuleOptions> | false;
}
}
19 changes: 10 additions & 9 deletions src/runtime/server/middleware/csrf.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,26 @@

import * as csrf from 'uncsrf'
import { defineEventHandler, getCookie, getHeader, createError } from 'h3'
import { useRuntimeConfig } from '#imports'
import { useRuntimeConfig, getRouteRules } from '#imports'
import { useSecretKey } from '../helpers'
import { defuReplaceArray } from '../../../utils'

const csrfConfig = useRuntimeConfig().csurf
const methodsToProtect = csrfConfig.methodsToProtect ?? []
const excludedUrls = csrfConfig.excludedUrls ?? []
const baseConfig = useRuntimeConfig().csurf

export default defineEventHandler(async (event) => {
const { csurf } = getRouteRules(event)
if (csurf === false || csurf?.enabled === false) { return } // csrf protection disabled for this route

const csrfConfig = defuReplaceArray(csurf, baseConfig)
const method = event.node.req.method ?? ''
const methodsToProtect = csrfConfig.methodsToProtect ?? []
if (!methodsToProtect.includes(method)) { return }

const secret = getCookie(event, csrfConfig.cookieKey!) ?? ''
const token = getHeader(event, 'csrf-token') ?? ''
// verify the incoming csrf token
const url = event.node.req.url ?? ''
const excluded = excludedUrls.some(el => Array.isArray(el)
? new RegExp(...el).test(url)
: el === url)
if (!excluded && !(await csrf.verify(secret, token, await useSecretKey(csrfConfig), csrfConfig.encryptAlgorithm))) {
const isValidToken = await csrf.verify(secret, token, await useSecretKey(csrfConfig), csrfConfig.encryptAlgorithm)
if (!isValidToken) {
throw createError({
statusCode: 403,
name: 'EBADCSRFTOKEN',
Expand Down
2 changes: 1 addition & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ export interface ModuleOptions {
cookie?: CookieSerializeOptions
cookieKey?: string
methodsToProtect?: Array<string> // the request methods we want CSRF protection for
excludedUrls?: Array<string|[string, string]> // any URLs we want to exclude from CSRF protection
encryptSecret?: string // for non serverless runtime
encryptAlgorithm?: EncryptAlgorithm
addCsrfTokenToEventCtx?: boolean // to run useCsrfFetch on server
enabled?: boolean // disabled module server middleware/plugin when `enabled` is set to `false` (you will still have access to `useCsrf`/`useCsrfFetch` client composables)
}
8 changes: 8 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { createDefu } from 'defu'

export const defuReplaceArray = createDefu((obj, key, value) => {
if (Array.isArray(obj[key]) || Array.isArray(value)) {
obj[key] = value
return true
}
})

0 comments on commit 7550de1

Please sign in to comment.