Skip to content

Commit

Permalink
Merge pull request #381 from Baroshem/chore/1.2.0
Browse files Browse the repository at this point in the history
Chore/1.2.0
  • Loading branch information
Baroshem authored Feb 22, 2024
2 parents 30f1e7b + ca69e00 commit 35d2352
Show file tree
Hide file tree
Showing 22 changed files with 187 additions and 44 deletions.
5 changes: 5 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"singleQuote": true,
"trailingComma": "none",
"semi": false
}
2 changes: 1 addition & 1 deletion .stackblitz/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@
"nuxt": "3.9.3"
},
"dependencies": {
"nuxt-security": "^1.1.2"
"nuxt-security": "^1.2.0"
}
}
8 changes: 4 additions & 4 deletions .stackblitz/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4101,10 +4101,10 @@ nuxt-csurf@^1.3.1:
defu "^6.1.1"
uncsrf "^1.1.1"

nuxt-security@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/nuxt-security/-/nuxt-security-1.1.2.tgz#08079f5cf55dc3f479be664c93195cd9c3b68c9f"
integrity sha512-Cdn8Cg5gJy+QO/QjlJgb1Gv03mtSZ6NP8Fb2LEjtsRqmWci6DDYI0D5TJYoj9SpVCYOiw9YVsawGX8HPK/YRMg==
nuxt-security@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/nuxt-security/-/nuxt-security-1.2.0.tgz#7b98bca275f45cb652466849413eee5bdc74657c"
integrity sha512-pAXW1vqMeMC6PnwzgE/Gf75BMOSQpSLbrgnZh9qmNf1Ytj7YI+Z180KZbkSXu944cFt6TGO0BDwlzc5Ij8uxPw==
dependencies:
"@nuxt/kit" "^3.8.0"
basic-auth "^2.0.1"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ security: {
exclude: [/node_modules/, /\.git/]
},
ssg: {
meta: true,
hashScripts: true,
hashStyles: false
},
Expand Down
2 changes: 2 additions & 0 deletions docs/content/1.documentation/2.headers/1.csp.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ export default defineNuxtConfig({
security: {
nonce: true, // Enables HTML nonce support in SSR mode
ssg: {
meta: true, // Enables CSP as a meta tag in SSG mode
hashScripts: true, // Enables CSP hash support for scripts in SSG mode
hashStyles: false // Disables CSP hash support for styles in SSG mode (recommended)
},
Expand Down Expand Up @@ -263,6 +264,7 @@ export default defineNuxtConfig({
// Global
security: {
ssg: {
meta: true, // Enables CSP as a meta tag in SSG mode
hashScripts: true, // Enables CSP hash support for scripts in SSG mode
hashStyles: false // Disables CSP hash support for styles in SSG mode (recommended)
},
Expand Down
17 changes: 15 additions & 2 deletions docs/content/1.documentation/3.middleware/3.xss-validator.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

:ellipsis{right=0px width=75% blur=150px}

This middleware works for both `GET`, `POST` methods and will throw an `400 Bad Request` error when the either body or query params will contain unsecure code. Based on <https://github.com/leizongmin/js-xss>
This middleware works by default for both `GET` and `POST` methods, and will throw a `400 Bad Request` error when either the body or the query params contain unsafe code. Based on <https://github.com/leizongmin/js-xss>

::alert{type="info"}
ℹ Read more about performing output escaping [here](https://cheatsheetseries.owasp.org/cheatsheets/Nodejs_Security_Cheat_Sheet.html#perform-output-escaping).
Expand Down Expand Up @@ -43,18 +43,25 @@ You can also disable the middleware globally or per route by setting `xssValidat

## Options

XSS validator accepts following configuration options:
XSS validator accepts the following configuration options:

```ts
type XssValidator = {
methods: Array<Uppercase<string>>;
whiteList: Record<string, any>;
escapeHtml: boolean;
stripIgnoreTag: boolean;
stripIgnoreTagBody: boolean;
css: Record<string, any> | boolean;
throwError: boolean;
} | {};
```

### `methods`
- Default: `['GET', 'POST']`

List of methods for which the validator will be invoked.

### `whiteList`

- Default: `-`
Expand All @@ -73,6 +80,12 @@ Filter out tags not in the whitelist

Filter out tags and tag bodies not in the whitelist

### `escapeHtml`

- Default: `-`

Disable html escaping (by default `<` is replaced by `&lt;` and `>` by `&gt;`)

### `css`

- Default: `-`
Expand Down
3 changes: 2 additions & 1 deletion docs/content/1.documentation/5.advanced/3.strict-csp.md
Original file line number Diff line number Diff line change
Expand Up @@ -569,7 +569,7 @@ Nuxt Security supports CSP via HTTP headers for Nitro Presets that output HTTP h
::alert{type="info"}
If you deploy your SSG site on Vercel or Netlify, you will benefit automatically from CSP Headers.
<br>
CSP will be delivered via HTTP headers, in addition to the standard `<meta http-equiv>` approach.
CSP will be delivered via HTTP headers, in addition to the standard `<meta http-equiv>` approach. If you want to disable the meta tag, so that only the HTTP headers are used, you can do so with the `ssg: meta` option.
::

### Per Route CSP
Expand Down Expand Up @@ -608,6 +608,7 @@ export default defineNuxtConfig({
security: {
nonce: true, // Enables HTML nonce support in SSR mode
ssg: {
meta: true, // Enables CSP as a meta tag in SSG mode
hashScripts: true, // Enables CSP hash support for scripts in SSG mode
hashStyles: false // Disables CSP hash support for styles in SSG mode (recommended)
},
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "nuxt-security",
"version": "1.1.2",
"version": "1.2.0",
"license": "MIT",
"type": "module",
"homepage": "https://nuxt-security.vercel.app",
Expand Down
2 changes: 1 addition & 1 deletion playground/nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,4 @@ export default defineNuxtConfig({
},
runtimeHooks: true
}
})
})
2 changes: 2 additions & 0 deletions src/defaultConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export const defaultSecurityConfig = (serverlUrl: string): Partial<ModuleOptions
...defaultThrowErrorValue
},
xssValidator: {
methods: ['GET', 'POST'],
...defaultThrowErrorValue
},
corsHandler: {
Expand Down Expand Up @@ -82,6 +83,7 @@ export const defaultSecurityConfig = (serverlUrl: string): Partial<ModuleOptions
exclude: [/node_modules/, /\.git/]
},
ssg: {
meta: true,
hashScripts: true,
hashStyles: false
},
Expand Down
2 changes: 2 additions & 0 deletions src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ export default defineNuxtModule<ModuleOptions>({
}

if (nuxt.options.security.xssValidator) {
// Remove potential duplicates
nuxt.options.security.xssValidator.methods = Array.from(new Set(nuxt.options.security.xssValidator.methods))
addServerHandler({
handler: normalize(
resolve(runtimeDir, 'server/middleware/xssValidator')
Expand Down
6 changes: 4 additions & 2 deletions src/runtime/nitro/plugins/04-cspSsgHashes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,10 @@ export default defineNitroPlugin((nitroApp) => {
// Generate CSP rules
const csp = security.headers.contentSecurityPolicy
const headerValue = generateCspRules(csp, scriptHashes, styleHashes)
// Insert CSP in the http meta tag
cheerios.head.push(cheerio.load(`<meta http-equiv="Content-Security-Policy" content="${headerValue}">`))
// Insert CSP in the http meta tag if meta is true
if (security.ssg?.meta) {
cheerios.head.push(cheerio.load(`<meta http-equiv="Content-Security-Policy" content="${headerValue}">`))
}
// Update rules in HTTP header
setResponseHeader(event, 'Content-Security-Policy', headerValue)
})
Expand Down
11 changes: 8 additions & 3 deletions src/runtime/server/middleware/xssValidator.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
import { FilterXSS } from 'xss'
import { FilterXSS, IFilterXSSOptions } from 'xss'
import { defineEventHandler, createError, getQuery, readBody, getRouteRules } from '#imports'
import { HTTPMethod } from '~/src/module'

export default defineEventHandler(async (event) => {
const { security } = getRouteRules(event)

if (security?.xssValidator) {
const xssValidator = new FilterXSS(security.xssValidator)
const filterOpt: IFilterXSSOptions = { ...security.xssValidator, escapeHtml: undefined }
if (security.xssValidator.escapeHtml === false) { // No html escaping (by default "<" is replaced by "&lt;" and ">" by "&gt;")
filterOpt.escapeHtml = (value: string) => value
}
const xssValidator = new FilterXSS(filterOpt)

if (event.node.req.socket.readyState !== 'readOnly') {
if (['POST', 'GET'].includes(event.node.req.method!)) {
if (security.xssValidator.methods && security.xssValidator.methods.includes(event.node.req.method! as HTTPMethod)) {
const valueToFilter =
event.node.req.method === 'GET'
? getQuery(event)
Expand Down
1 change: 1 addition & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { SecurityHeaders } from './headers'
import type { AllowedHTTPMethods, BasicAuth, RateLimiter, RequestSizeLimiter, XssValidator, CorsOptions } from './middlewares'

export type Ssg = {
meta?: boolean;
hashScripts?: boolean;
hashStyles?: boolean;
};
Expand Down
7 changes: 6 additions & 1 deletion src/types/middlewares.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,12 @@ export type RateLimiter = {
};

export type XssValidator = {
/** Array of methods for which the validator will be invoked.
@default ['GET', 'POST']
*/
methods?: Array<HTTPMethod>;
whiteList?: Record<string, any>;
escapeHtml?: boolean;
stripIgnoreTag?: boolean;
stripIgnoreTagBody?: boolean;
css?: Record<string, any> | boolean;
Expand All @@ -32,7 +37,7 @@ export type BasicAuth = {
message: string;
}

export type HTTPMethod = 'GET' | 'POST' | 'DELETE' | 'PATCH' | 'POST' | string;
export type HTTPMethod = 'GET' | 'POST' | 'DELETE' | 'PATCH' | 'PUT' | 'TRACE' | 'OPTIONS' | 'CONNECT' | 'HEAD';

// Cannot use the H3CorsOptions from `h3` as it breaks the build process for some reason :(
export type CorsOptions = {
Expand Down
72 changes: 52 additions & 20 deletions test/fixtures/perRoute/nuxt.config.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
export default defineNuxtConfig({
modules: [
'../../../src/module'
],
modules: ['../../../src/module'],
routeRules: {
'/**': {
headers: {
'foo': 'bar',
foo: 'bar',
'Referrer-Policy': 'no-referrer-when-downgrade'
}
},
Expand All @@ -19,7 +17,7 @@ export default defineNuxtConfig({
headers: {
crossOriginOpenerPolicy: 'same-origin-allow-popups'
}
},
}
},
'/ignore-specific/**': {
security: {
Expand All @@ -34,15 +32,15 @@ export default defineNuxtConfig({
headers: {
crossOriginResourcePolicy: false,
crossOriginOpenerPolicy: undefined,
crossOriginEmbedderPolicy: 'credentialless',
crossOriginEmbedderPolicy: 'credentialless'
}
}
},
'merge-recursive/deep/**': {
security: {
headers: {
crossOriginResourcePolicy: 'same-site',
crossOriginOpenerPolicy: false,
crossOriginOpenerPolicy: false
}
}
},
Expand All @@ -51,9 +49,10 @@ export default defineNuxtConfig({
'Cross-Origin-Resource-Policy': 'cross-origin',
'Strict-Transport-Security': 'max-age=1; preload;',
'Permissions-Policy': 'fullscreen=*, camera=(self)',
'Content-Security-Policy': "script-src 'self' https:; media-src 'none';",
'foo': 'baz',
'foo2': 'baz2'
'Content-Security-Policy':
"script-src 'self' https:; media-src 'none';",
foo: 'baz',
foo2: 'baz2'
}
},
'/resolve-conflict/**': {
Expand All @@ -63,8 +62,8 @@ export default defineNuxtConfig({
'Cross-Origin-Embedder-Policy': 'unsafe-none',
'Strict-Transport-Security': 'max-age=1; preload;',
'Permissions-Policy': 'fullscreen=*',
'foo': 'baz',
'foo2': 'baz2'
foo: 'baz',
foo2: 'baz2'
},
security: {
headers: {
Expand Down Expand Up @@ -118,9 +117,9 @@ export default defineNuxtConfig({
},
//@ts-ignore - Intentional as we test backwards compatibility with a deprecated syntax
'Content-Security-Policy': {
"base-uri": false,
"script-src": "'self'",
"img-src": ['https:']
'base-uri': false,
'script-src': "'self'",
'img-src': ['https:']
}
}
},
Expand Down Expand Up @@ -198,7 +197,7 @@ export default defineNuxtConfig({
nonce: false,
headers: {
contentSecurityPolicy: {
"script-src": ["'nonce-{{nonce}}'"]
'script-src': ["'nonce-{{nonce}}'"]
}
}
}
Expand All @@ -214,16 +213,44 @@ export default defineNuxtConfig({
}
},
'/csp-hash/deep/disabled': {
prerender: true
prerender: true,
security: {
ssg: {
meta: true,
hashScripts: false
}
}
},
'/csp-hash/deep/enabled': {
prerender: true,
security: {
ssg: {
meta: true,
hashScripts: true
}
}
},
'/csp-meta/**': {
security: {
ssg: false
}
},
'/csp-meta/deep/disabled': {
prerender: true,
security: {
ssg: {
meta: false,
}
}
},
'/csp-meta/deep/enabled': {
prerender: true,
security: {
ssg: {
meta: true,
}
}
},
'/sri-attribute/**': {
security: {
sri: false
Expand Down Expand Up @@ -254,15 +281,20 @@ export default defineNuxtConfig({
},
security: {
headers: {
contentSecurityPolicy: false,
contentSecurityPolicy: false
}
}
},
}
},
security: {
headers: {
contentSecurityPolicy: {
"script-src": ["'self'", 'https:', "'unsafe-inline'", "'strict-dynamic'"]
'script-src': [
"'self'",
'https:',
"'unsafe-inline'",
"'strict-dynamic'"
]
}
}
}
Expand Down
3 changes: 3 additions & 0 deletions test/fixtures/perRoute/pages/csp-meta/deep/disabled.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/perRoute/pages/csp-meta/deep/enabled.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<template>
<div>basic</div>
</template>
Loading

0 comments on commit 35d2352

Please sign in to comment.