Skip to content

Commit

Permalink
feat(core): introduce owasp default sets
Browse files Browse the repository at this point in the history
<!--- Provide a general summary of your changes in the title above -->

Closes #470

## Types of changes
<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->
- [ ] Bug fix (a non-breaking change which fixes an issue)
- [x] New feature (a non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to change)

## Description
<!--- Describe your changes in detail -->
<!--- Why is this change required? What problem does it solve? -->
<!--- If it resolves an open issue, please link to the issue here. For example "Resolves: #137" -->

This PR adds a new `owaspDefaults` option, which can take 2 possible values:
- `compatibility` (default): OWASP default settings are chosen to minimize the possibility of breaking the app. These default values are the same as in v1.
- `security`: OWASP default settings are chosen to maximize security. These default values will usually require some additional fine-tuning to ensure the app will run smoothly.

With `security` OWASP level, the following headers are modified:
1- `contentSecurityPolicy` blocks everything by default with `default-src: 'none'`. In addition, all `'unsafe-inline'` values are removed.
2- `crossOriginEmbedderPolicy` is set to `require-corp`
3- `strictTransportSecurity` has the `preload` flag
4- 'xFrameOptions` is set to `DENY`

## Checklist:
<!--- Put an `x` in all the boxes that apply. -->
<!--- If your change requires a documentation PR, please link it appropriately -->
<!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->
- [x] My change requires a change to the documentation.
- [ ] I have updated the documentation accordingly.
- [x] I have added tests to cover my changes (if not applicable, please state why)
  • Loading branch information
vejja committed Jun 26, 2024
1 parent 1c33843 commit ce76b67
Show file tree
Hide file tree
Showing 15 changed files with 421 additions and 164 deletions.
182 changes: 109 additions & 73 deletions src/defaultConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,102 @@ import type { ModuleOptions } from './types/module'

const defaultThrowErrorValue = { throwError: true }

export const defaultSecurityConfig = (serverlUrl: string): ModuleOptions => ({
headers: {
crossOriginResourcePolicy: 'same-origin',
crossOriginOpenerPolicy: 'same-origin',
crossOriginEmbedderPolicy: 'require-corp',
contentSecurityPolicy: {

export const defaultSecurityConfig = (serverlUrl: string, owaspDefaults: ModuleOptions['owaspDefaults']) => {
const defaults: any = {
owaspDefaults,
headers: {
crossOriginResourcePolicy: 'same-origin',
crossOriginOpenerPolicy: 'same-origin',
crossOriginEmbedderPolicy: 'credentialless',
contentSecurityPolicy: {
'base-uri': ["'none'"],
'font-src': ["'self'", 'https:', 'data:'],
'form-action': ["'self'"],
'frame-ancestors': ["'self'"],
'img-src': ["'self'", 'data:'],
'object-src': ["'none'"],
'script-src-attr': ["'none'"],
'style-src': ["'self'", 'https:', "'unsafe-inline'"],
'script-src': ["'self'", 'https:', "'unsafe-inline'", "'strict-dynamic'", "'nonce-{{nonce}}'"],
'upgrade-insecure-requests': true
},
originAgentCluster: '?1',
referrerPolicy: 'no-referrer',
strictTransportSecurity: {
maxAge: 15552000,
includeSubdomains: true,
},
xContentTypeOptions: 'nosniff',
xDNSPrefetchControl: 'off',
xDownloadOptions: 'noopen',
xFrameOptions: 'SAMEORIGIN',
xPermittedCrossDomainPolicies: 'none',
xXSSProtection: '0',
permissionsPolicy: {
camera: [],
'display-capture': [],
fullscreen: [],
geolocation: [],
microphone: []
}
},
requestSizeLimiter: {
maxRequestSizeInBytes: 2000000,
maxUploadFileRequestInBytes: 8000000,
...defaultThrowErrorValue
},
rateLimiter: {
// Twitter search rate limiting
tokensPerInterval: 150,
interval: 300_000,
headers: false,
driver: {
name: 'lruCache'
},
...defaultThrowErrorValue
},
xssValidator: {
methods: ['GET', 'POST'],
...defaultThrowErrorValue
},
corsHandler: {
// Options by CORS middleware for Express https://github.com/expressjs/cors#configuration-options
origin: serverlUrl,
methods: ['GET', 'HEAD', 'PUT', 'PATCH', 'POST', 'DELETE'],
preflight: {
statusCode: 204
}
},
allowedMethodsRestricter: {
methods: '*',
...defaultThrowErrorValue
},
hidePoweredBy: true,
basicAuth: false,
enabled: true,
csrf: false,
nonce: true,
// https://github.com/Talljack/unplugin-remove/blob/main/src/types.ts
removeLoggers: {
external: [],
consoleType: ['log', 'debug'],
include: [/\.[jt]sx?$/, /\.vue\??/],
exclude: [/node_modules/, /\.git/]
},
ssg: {
meta: true,
hashScripts: true,
hashStyles: false,
nitroHeaders: true,
exportToPresets: true,
},
sri: true
}

if (owaspDefaults === 'security') {
defaults.headers.crossOriginEmbedderPolicy = 'require-corp'
defaults.headers.contentSecurityPolicy = {
'base-uri': ["'none'"],
'default-src' : ["'none'"],
'connect-src': ["'self'", 'https:'],
Expand All @@ -20,24 +110,18 @@ export const defaultSecurityConfig = (serverlUrl: string): ModuleOptions => ({
'media-src': ["'self'"],
'object-src': ["'none'"],
'script-src-attr': ["'none'"],
'style-src': ["'self'", 'https:', "'unsafe-inline'"],
'script-src': ["'self'", 'https:', "'unsafe-inline'", "'strict-dynamic'", "'nonce-{{nonce}}'"],
'style-src': ["'self'", 'https:', "'nonce-{{nonce}}'"],
'script-src': ["'self'", 'https:', "'strict-dynamic'", "'nonce-{{nonce}}'"],
'upgrade-insecure-requests': true,
'worker-src': ["'self'"],
},
originAgentCluster: '?1',
referrerPolicy: 'no-referrer',
strictTransportSecurity: {
}
defaults.headers.strictTransportSecurity = {
maxAge: 31536000,
includeSubdomains: true
includeSubdomains: true,
preload: true
},
xContentTypeOptions: 'nosniff',
xDNSPrefetchControl: 'off',
xDownloadOptions: 'noopen',
xFrameOptions: 'DENY',
xPermittedCrossDomainPolicies: 'none',
xXSSProtection: '0',
permissionsPolicy: {
defaults.headers.xFrameOptions = 'DENY'
defaults.headers.permissionsPolicy = {
accelerometer: [],
/* Disable OWASP Experimental values
'ambient-light-sensor':[],
Expand Down Expand Up @@ -88,56 +172,8 @@ export const defaultSecurityConfig = (serverlUrl: string): ModuleOptions => ({
'web-share':[],
'xr-spatial-tracking':[]
}
},
requestSizeLimiter: {
maxRequestSizeInBytes: 2000000,
maxUploadFileRequestInBytes: 8000000,
...defaultThrowErrorValue
},
rateLimiter: {
// Twitter search rate limiting
tokensPerInterval: 150,
interval: 300000,
headers: false,
driver: {
name: 'lruCache'
},
...defaultThrowErrorValue
},
xssValidator: {
methods: ['GET', 'POST'],
...defaultThrowErrorValue
},
corsHandler: {
// Options by CORS middleware for Express https://github.com/expressjs/cors#configuration-options
origin: serverlUrl,
methods: ['GET', 'HEAD', 'PUT', 'PATCH', 'POST', 'DELETE'],
preflight: {
statusCode: 204
}
},
allowedMethodsRestricter: {
methods: '*',
...defaultThrowErrorValue
},
hidePoweredBy: true,
basicAuth: false,
enabled: true,
csrf: false,
nonce: true,
// https://github.com/Talljack/unplugin-remove/blob/main/src/types.ts
removeLoggers: {
external: [],
consoleType: ['log', 'debug'],
include: [/\.[jt]sx?$/, /\.vue\??/],
exclude: [/node_modules/, /\.git/]
},
ssg: {
meta: true,
hashScripts: true,
hashStyles: false,
nitroHeaders: true,
exportToPresets: true,
},
sri: true
})
}
return defaults as ModuleOptions
}


3 changes: 2 additions & 1 deletion src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,11 @@ export default defineNuxtModule<ModuleOptions>({
nuxt.options.build.transpile.push(resolver.resolve('./runtime'))

// First merge module options with default options
const defaultLevel = options.owaspDefaults || nuxt.options.security?.owaspDefaults || 'normal'
nuxt.options.security = defuReplaceArray(
{ ...options, ...nuxt.options.security },
{
...defaultSecurityConfig(nuxt.options.devServer.url)
...defaultSecurityConfig(nuxt.options.devServer.url, defaultLevel)
}
)

Expand Down
3 changes: 2 additions & 1 deletion src/types/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export type Ssg = {
};

export interface ModuleOptions {
owaspDefaults: 'compatibility' | 'security';
headers: SecurityHeaders | false;
requestSizeLimiter: RequestSizeLimiter | false;
rateLimiter: RateLimiter | false;
Expand All @@ -31,7 +32,7 @@ export interface ModuleOptions {
}

export type NuxtSecurityRouteRules = Partial<
Omit<ModuleOptions, 'csrf' | 'basicAuth' | 'rateLimiter' | 'ssg' | 'requestSizeLimiter' >
Omit<ModuleOptions, 'defaultLevel' | 'csrf' | 'basicAuth' | 'rateLimiter' | 'ssg' | 'requestSizeLimiter' >
& { rateLimiter: Omit<RateLimiter, 'driver'> | false }
& { ssg: Omit<Ssg, 'exportToPresets'> | false }
& { requestSizeLimiter: RequestSizeLimiter | false }
Expand Down
4 changes: 2 additions & 2 deletions test/fixtures/perRoute/nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export default defineNuxtConfig({
headers: {
crossOriginResourcePolicy: false,
crossOriginOpenerPolicy: undefined,
crossOriginEmbedderPolicy: 'credentialless'
crossOriginEmbedderPolicy: 'unsafe-none'
}
}
},
Expand Down Expand Up @@ -68,7 +68,7 @@ export default defineNuxtConfig({
security: {
headers: {
crossOriginOpenerPolicy: false,
crossOriginEmbedderPolicy: 'credentialless',
crossOriginEmbedderPolicy: 'unsafe-none',
strictTransportSecurity: {
maxAge: 2,
preload: false,
Expand Down
1 change: 1 addition & 0 deletions test/fixtures/settingsMode/.nuxtrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
imports.autoImport=true
5 changes: 5 additions & 0 deletions test/fixtures/settingsMode/app.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<template>
<div>
<NuxtPage />
</div>
</template>
15 changes: 15 additions & 0 deletions test/fixtures/settingsMode/nuxt.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export default defineNuxtConfig({
modules: [
'../../../src/module'
],
routeRules: {
'/test': {
headers: {
'x-xss-protection': '1',
}
}
},
security: {
owaspDefaults: 'security',
}
})
5 changes: 5 additions & 0 deletions test/fixtures/settingsMode/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"private": true,
"name": "basic",
"type": "module"
}
3 changes: 3 additions & 0 deletions test/fixtures/settingsMode/pages/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<template>
<div>basic</div>
</template>
3 changes: 3 additions & 0 deletions test/fixtures/settingsMode/pages/test.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<template>
<div>test</div>
</template>
10 changes: 5 additions & 5 deletions test/headers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ describe('[nuxt-security] Headers', async () => {
expect(cspHeaderValue).toBeTruthy()
expect(nonceValue).toBeDefined()
expect(nonceValue).toHaveLength(24)
expect(cspHeaderValue).toBe(`base-uri 'none'; default-src 'none'; connect-src 'self' https:; font-src 'self' https: data:; form-action 'self'; frame-ancestors 'self'; frame-src 'self'; img-src 'self' data:; manifest-src 'self'; media-src 'self'; object-src 'none'; script-src-attr 'none'; style-src 'self' https: 'unsafe-inline'; script-src 'self' https: 'unsafe-inline' 'strict-dynamic' 'nonce-${nonceValue}'; upgrade-insecure-requests; worker-src 'self';`)
expect(cspHeaderValue).toBe(`base-uri 'none'; font-src 'self' https: data:; form-action 'self'; frame-ancestors 'self'; img-src 'self' data:; object-src 'none'; script-src-attr 'none'; style-src 'self' https: 'unsafe-inline'; script-src 'self' https: 'unsafe-inline' 'strict-dynamic' 'nonce-${nonceValue}'; upgrade-insecure-requests;`)
})

it('has `cross-origin-embedder-policy` header set with correct default value', async () => {
Expand All @@ -48,7 +48,7 @@ describe('[nuxt-security] Headers', async () => {
const coepHeaderValue = headers.get('cross-origin-embedder-policy')

expect(coepHeaderValue).toBeTruthy()
expect(coepHeaderValue).toBe('require-corp')
expect(coepHeaderValue).toBe('credentialless')
})

it('has `cross-origin-opener-policy` header set with correct default value', async () => {
Expand Down Expand Up @@ -92,7 +92,7 @@ describe('[nuxt-security] Headers', async () => {
const ppHeaderValue = headers.get('permissions-policy')

expect(ppHeaderValue).toBeTruthy()
expect(ppHeaderValue).toBe('accelerometer=(), autoplay=(), camera=(), display-capture=(), encrypted-media=(), fullscreen=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), midi=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(self), usb=(), web-share=(), xr-spatial-tracking=()')
expect(ppHeaderValue).toBe('camera=(), display-capture=(), fullscreen=(), geolocation=(), microphone=()')
})

it('has `referrer-policy` header set with correct default value', async () => {
Expand All @@ -114,7 +114,7 @@ describe('[nuxt-security] Headers', async () => {
const stsHeaderValue = headers.get('strict-transport-security')

expect(stsHeaderValue).toBeTruthy()
expect(stsHeaderValue).toBe('max-age=31536000; includeSubDomains;')
expect(stsHeaderValue).toBe('max-age=15552000; includeSubDomains;')
})

it('has `x-content-type-options` header set with correct default value', async () => {
Expand Down Expand Up @@ -158,7 +158,7 @@ describe('[nuxt-security] Headers', async () => {
const xfoHeaderValue = headers.get('x-frame-options')

expect(xfoHeaderValue).toBeTruthy()
expect(xfoHeaderValue).toBe('DENY')
expect(xfoHeaderValue).toBe('SAMEORIGIN')
})

it('has `x-permitted-cross-domain-policies` header set with correct default value', async () => {
Expand Down
Loading

0 comments on commit ce76b67

Please sign in to comment.