From 047b8cfa476dac00a15e58a80ceb93d0b3550ae4 Mon Sep 17 00:00:00 2001 From: Baroshem Date: Mon, 20 Mar 2023 12:41:58 +0100 Subject: [PATCH 1/5] feat: #109 routeRules support --- .stackblitz/nuxt.config.ts | 2 +- docs/content/1.getting-started/1.setup.md | 4 +- .../1.getting-started/2.configuration.md | 215 ++++++------- docs/content/2.security/1.headers.md | 138 +++----- .../2.security/2.request-size-limiter.md | 26 +- docs/content/2.security/3.rate-limiter.md | 27 +- docs/content/2.security/4.xss-validator.md | 21 +- docs/content/2.security/5.cors-handler.md | 23 +- .../6.allowed-methods-restricter.md | 19 +- docs/content/2.security/7.basic-auth.md | 11 +- playground/nuxt.config.ts | 86 ++--- playground/pages/secret.vue | 3 + src/defaultConfig.ts | 12 + src/module.ts | 304 +++++++++++------- .../middleware/allowedMethodsRestricter.ts | 23 +- src/runtime/server/middleware/basicAuth.ts | 2 +- src/runtime/server/middleware/corsHandler.ts | 7 +- src/runtime/server/middleware/rateLimiter.ts | 39 +-- .../server/middleware/requestSizeLimiter.ts | 37 ++- src/runtime/server/middleware/xssValidator.ts | 32 +- src/types.ts | 20 +- 21 files changed, 549 insertions(+), 502 deletions(-) create mode 100644 playground/pages/secret.vue diff --git a/.stackblitz/nuxt.config.ts b/.stackblitz/nuxt.config.ts index 387af7af..9df90af6 100644 --- a/.stackblitz/nuxt.config.ts +++ b/.stackblitz/nuxt.config.ts @@ -1,7 +1,7 @@ // https://v3.nuxtjs.org/api/configuration/nuxt.config export default defineNuxtConfig({ modules: ['nuxt-security'], - // Following configuration is only necessary to make Stackblitz work correctly. + // Following configuration is only necessary to make Stackblitz work correctly. // For local projects, you do not need any configuration to try it out. security: { headers: { diff --git a/docs/content/1.getting-started/1.setup.md b/docs/content/1.getting-started/1.setup.md index 90ff8773..63af4ea5 100644 --- a/docs/content/1.getting-started/1.setup.md +++ b/docs/content/1.getting-started/1.setup.md @@ -45,7 +45,7 @@ You can find more about configuring Content Security Policy (CSP) [here](/securi ## Configuration -You can add configuration to the module like following: +You can add global configuration to the module like following: ```js{}[nuxt.config.ts] export default defineNuxtConfig({ @@ -55,6 +55,8 @@ export default defineNuxtConfig({ }) ``` +Or per route configuration described [here](/getting-started/configuration#per-route-middleware-configuration) + ::alert{type="info"} You can find more about configuring `nuxt-security` [here](/getting-started/configuration). :: diff --git a/docs/content/1.getting-started/2.configuration.md b/docs/content/1.getting-started/2.configuration.md index 63afc0c0..05e4eb47 100644 --- a/docs/content/1.getting-started/2.configuration.md +++ b/docs/content/1.getting-started/2.configuration.md @@ -1,54 +1,86 @@ --- title: Configuration -description: '' +description: "" --- -The module by default will register middlewares and route roules to make your application more secure. If you need, you can also modify or disable any of middlewares/routes if you do not need them or your project cannot use them (i.e. some Statically Generated websites will not be able to use middlewares). +The module by default will register **global** middlewares and route roules to make your application more secure. You can also modify or disable any of middlewares/routes or your project cannot use them (i.e. some Statically Generated websites will not be able to use middlewares). -You can add configuration to the module like following: +You can add **global** configuration to the module like following: ```js{}[nuxt.config.ts] export default defineNuxtConfig({ security: { - requestSizeLimiter: { - value: { - maxRequestSizeInBytes: 3000000, - maxUploadFileRequestInBytes: 9000000, - }, - route: '/upload-file' + rateLimiter: { + tokensPerInterval: 2, + interval: 'hour', } - // Other options + } +}) +``` + +In general, the `security` object in nuxt configuration should be used to register functionality that will be used **globally** in your application. For per route configuration, check out the next section. + +## Per route middleware configuration + +By default, middlewares are configured to work globally, but you can easily configure them per route by using `routeRules`: + +```js{}[nuxt.config.ts] +export default defineNuxtConfig({ + routeRules: { + '/custom-route': { + security: { + rateLimiter: { + tokensPerInterval: 2, + interval: 'hour', + } + } + } + } +}) +``` + +By adding this you will have global middleware for all routes (regarding rate limiting) and specific configuration to the `/custom-route` route. + +To avoid conflicts with global middlewares, you can disable them from the module configuration like following: + +```js{}[nuxt.config.ts] +export default defineNuxtConfig({ + security: { + rateLimiter: false } }) ``` ## Configuration Types -Each middleware configuration object is build using same TS type: +::alert{type="warning"} +The following previous interface for registering middlewares is now deprecated due to the introduction of `per-route` configuration. ```ts type MiddlewareConfiguration = { value: MIDDLEWARE; route: string; -} +}; ``` -* `value` is the value of certain header or middleware. It may be a simple string or an object depending on the method. -* `route` is the route that should this header or middleware be attached to. By default for routeRoules (headers) the route is `/**` and for middlewares is `''` (empty string) -> global middleware. +Make sure to use the `security` object in `nuxt.config.ts` to register global functionality and `routeRules` for per-route configuration. + +:: All module configuration is the following type: ```ts interface ModuleOptions { headers: SecurityHeaders | false; - requestSizeLimiter: MiddlewareConfiguration | false; - rateLimiter: MiddlewareConfiguration | false; - xssValidator: MiddlewareConfiguration | false; - corsHandler: MiddlewareConfiguration | false; - allowedMethodsRestricter: MiddlewareConfiguration | false; + requestSizeLimiter: RequestSizeLimiter | false; + rateLimiter: RateLimiter | false; + xssValidator: XssValidator | false; + corsHandler: CorsOptions | false; + allowedMethodsRestricter: AllowedHTTPMethods | false; hidePoweredBy: boolean; - basicAuth: MiddlewareConfiguration | boolean; + basicAuth: BasicAuth | boolean; enabled: boolean; + csrf: CsrfOptions | boolean; } ``` @@ -61,127 +93,67 @@ This module will by default set the following configuration options to enable mi ```ts security: { headers: { - crossOriginResourcePolicy: { - value: 'same-origin', - route: '/**' - }, - crossOriginOpenerPolicy: { - value: 'same-origin', - route: '/**' - }, - crossOriginEmbedderPolicy: { - value: 'require-corp', - route: '/**' - }, + crossOriginResourcePolicy: 'same-origin', + crossOriginOpenerPolicy: 'same-origin', + crossOriginEmbedderPolicy: 'require-corp', contentSecurityPolicy: { - value: { - 'base-uri': ["'self'"], - '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'"], - 'upgrade-insecure-requests': true - }, - route: '/**' - }, - originAgentCluster: { - value: '?1', - route: '/**' - }, - referrerPolicy: { - value: 'no-referrer', - route: '/**' - }, + 'base-uri': ["'self'"], + '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'"], + 'upgrade-insecure-requests': true + }, + originAgentCluster: '?1', + referrerPolicy: 'no-referrer', strictTransportSecurity: { - value: { - maxAge: 15552000, - includeSubdomains: true - }, - route: '/**' - }, - xContentTypeOptions: { - value: 'nosniff', - route: '/**' - }, - xDNSPrefetchControl: { - value: 'off', - route: '/**' - }, - xDownloadOptions: { - value: 'noopen', - route: '/**' - }, - xFrameOptions: { - value: 'SAMEORIGIN', - route: '/**' - }, - xPermittedCrossDomainPolicies: { - value: 'none', - route: '/**' - }, - xXSSProtection: { - value: '0', - route: '/**' - }, - permissionsPolicy: { - value: { - 'camera': ['()'], - 'display-capture': ['()'], - 'fullscreen': ['()'], - 'geolocation': ['()'], - 'microphone': ['()'], - }, - route: '/**' + maxAge: 15552000, + includeSubdomains: true + }, + xContentTypeOptions: 'nosniff', + xDNSPrefetchControl: 'off', + xDownloadOptions: 'noopen', + xFrameOptions: 'SAMEORIGIN', + xPermittedCrossDomainPolicies: 'none', + xXSSProtection: '0', + permissionsPolicy: { + 'camera': ['()'], + 'display-capture': ['()'], + 'fullscreen': ['()'], + 'geolocation': ['()'], + 'microphone': ['()'], } }, requestSizeLimiter: { - value: { maxRequestSizeInBytes: 2000000, maxUploadFileRequestInBytes: 8000000, - }, - route: '', - throwError?: true, }, rateLimiter: { // Twitter search rate limiting - value: { tokensPerInterval: 150, interval: "hour", fireImmediately: true, - }, - route: '', - throwError?: true, - }, - xssValidator: { - value: {}, - route: '', - throwError?: true, }, + xssValidator: {}, corsHandler: { - value: { - origin: '*', - methods: ['GET','HEAD','PUT','PATCH','POST','DELETE'], - preflight: { - statusCode: 204 - } - }, - route: '', - }, - allowedMethodsRestricter: { - value: '*', - route: '', - throwError?: true, + origin: '*', + methods: ['GET','HEAD','PUT','PATCH','POST','DELETE'], + preflight: { + statusCode: 204 + } }, + allowedMethodsRestricter: '*', hidePoweredBy: true, basicAuth: false, enabled: true, + csrf: false, } ``` -To read more about every security middleware, go to that middleware page in middlewares section. +To read more about every security middleware, go to that middleware page in `security` section. ## Using with Nuxt DevTools @@ -192,10 +164,7 @@ export default defineNuxtConfig({ modules: ['nuxt-security', '@nuxt/devtools'], security: { headers: { - crossOriginEmbedderPolicy: { - value: process.env.NODE_ENV === 'development' ? 'unsafe-none' : 'require-corp', - route: '/**', - } + crossOriginEmbedderPolicy: process.env.NODE_ENV === 'development' ? 'unsafe-none' : 'require-corp', }, }, }); diff --git a/docs/content/2.security/1.headers.md b/docs/content/2.security/1.headers.md index 4d7ae8b3..66100b46 100644 --- a/docs/content/2.security/1.headers.md +++ b/docs/content/2.security/1.headers.md @@ -3,45 +3,17 @@ title: Headers description: '' --- -A set of Nuxt `routeRoules` that will add appriopriate security headers to your response that will make your application more secure by default. All headers can be overriden by using the module configuration or by overriding certain routes. +A set of **global** Nuxt `routeRoules` that will add appriopriate security headers to your response that will make your application more secure by default. All headers can be overriden by using the module configuration. Check out all the available types [here](https://github.com/Baroshem/nuxt-security/blob/main/src/types.ts). It will help you solve [this](https://cheatsheetseries.owasp.org/cheatsheets/Nodejs_Security_Cheat_Sheet.html#use-appropriate-security-headers) security problem. -```ts -type SecurityHeaders = { - crossOriginResourcePolicy: MiddlewareConfiguration | false; - crossOriginOpenerPolicy: MiddlewareConfiguration | false; - crossOriginEmbedderPolicy: MiddlewareConfiguration | false; - contentSecurityPolicy: MiddlewareConfiguration | false; - originAgentCluster: MiddlewareConfiguration<'?1'> | false; - referrerPolicy: MiddlewareConfiguration | false; - strictTransportSecurity: MiddlewareConfiguration | false; - xContentTypeOptions: MiddlewareConfiguration | false; - xDNSPrefetchControl: MiddlewareConfiguration | false; - xDownloadOptions: MiddlewareConfiguration | false; - xFrameOptions: MiddlewareConfiguration | false; - xPermittedCrossDomainPolicies: MiddlewareConfiguration | false; - xXSSProtection: MiddlewareConfiguration | false; - permissionsPolicy: MiddlewareConfiguration | false; -} - -type MiddlewareConfiguration = { - value: MIDDLEWARE; - route: string; -} -``` - -To write a custom logic for this middleware follow this pattern: +To write a custom logic for these global headers follow this pattern: ```js{}[nuxt.config.ts] export default defineNuxtConfig({ - modules: ['nuxt-security'], security: { headers: { - xXSSProtection: { - value: '1', - route: '/my-custom-route' - }, + xXSSProtection: '1', contentSecurityPolicy: false } } @@ -51,6 +23,22 @@ export default defineNuxtConfig({ Note that setting values for certain headers only overrides the defaults for these particular headers. In order to disable given header, pass `false` - like the example above shows for `contentSecurityPolicy`. +To enable per-route configuration, use the `routeRules` like following: + +```ts +export default defineNuxtConfig({ + routeRules: { + '/custom-route': { + headers: { + 'Cross-Origin-Embedder-Policy': 'require-corp' + } + } + } +}) +``` + +When using `routeRules`, make sure to use the proper HTTP Header names like `Cross-Origin-Embedder-Policy` instead of `crossOriginEmbedderPolicy`. + ## `Content-Security-Policy` Content Security Policy (CSP) helps prevent unwanted content from being injected/loaded into your webpages. This can mitigate cross-site scripting (XSS) vulnerabilities, clickjacking, formjacking, malicious frames, unwanted trackers, and other web client-side attacks. @@ -61,18 +49,15 @@ Default value: ```ts contentSecurityPolicy: { - value: { - 'base-uri': ["'self'"], - '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'"], - 'upgrade-insecure-requests': true - }, - route: '/**' + 'base-uri': ["'self'"], + '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'"], + 'upgrade-insecure-requests': true } ``` @@ -85,10 +70,7 @@ Read more about this header [here](https://developer.mozilla.org/en-US/docs/Web/ Default value: ```ts -crossOriginEmbedderPolicy: { - value: 'require-corp', - route: '/**', -}, +crossOriginEmbedderPolicy: 'require-corp', ``` ## `Cross-Origin-Opener-Policy` @@ -100,10 +82,7 @@ Read more about this header [here](https://developer.mozilla.org/en-US/docs/Web/ Default value: ```ts -crossOriginOpenerPolicy: { - value: 'same-origin', - route: '/**', -}, +crossOriginOpenerPolicy: 'same-origin', ``` ## `Cross-Origin-Resource-Policy` @@ -115,10 +94,7 @@ Read more about this header [here](https://developer.mozilla.org/en-US/docs/Web/ Default value: ```ts -crossOriginResourcePolicy: { - value: 'same-origin', - route: '/**', -}, +crossOriginResourcePolicy: 'same-origin', ``` ## `Origin-Agent-Cluster` @@ -130,10 +106,7 @@ Read more about this header [here](https://web.dev/origin-agent-cluster). Default value: ```ts -originAgentCluster: { - value: '?1', - route: '/**', -}, +originAgentCluster: '?1', ``` ## `Referrer-Policy` @@ -145,10 +118,7 @@ Read more about this header [here](https://developer.mozilla.org/en-US/docs/Web/ Default value: ```ts -referrerPolicy: { - value: 'no-referrer', - route: '/**', -}, +referrerPolicy: 'no-referrer', ``` ## `Strict-Transport-Security` @@ -160,10 +130,7 @@ Read more about this header [here](https://developer.mozilla.org/en-US/docs/Web/ Default value: ```ts -strictTransportSecurity: { - value: 'max-age=15552000; includeSubDomains', - route: '/**', -}, +strictTransportSecurity: 'max-age=15552000; includeSubDomains', ``` ## `X-Content-Type-Options` @@ -175,10 +142,7 @@ Read more about this header [here](https://developer.mozilla.org/en-US/docs/Web/ Default value: ```ts -xContentTypeOptions: { - value: 'nosniff', - route: '/**', -}, +xContentTypeOptions: 'nosniff', ``` ## `X-DNS-Prefetch-Control` @@ -190,10 +154,7 @@ Read more about this header [here](https://developer.mozilla.org/en-US/docs/Web/ Default value: ```ts -xDNSPrefetchControl: { - value: 'off', - route: '/**', -}, +xDNSPrefetchControl: 'off', ``` ## `X-Download-Options` @@ -205,10 +166,7 @@ Read more about this header [here](https://webtechsurvey.com/response-header/x-d Default value: ```ts -xDownloadOptions: { - value: 'noopen', - route: '/**', -}, +xDownloadOptions: 'noopen', ``` ## `X-Frame-Options` @@ -220,10 +178,7 @@ Read more about this header [here](https://developer.mozilla.org/en-US/docs/Web/ Default value: ```ts -xFrameOptions: { - value: 'SAMEORIGIN', - route: '/**', -}, +xFrameOptions: 'SAMEORIGIN', ``` ## `X-Permitted-Cross-Domain-Policies` @@ -235,10 +190,7 @@ Read more about this header [here](https://www.scip.ch/en/?labs.20180308#:~:text Default value: ```ts -xPermittedCrossDomainPolicies: { - value: 'none', - route: '/**', -}, +xPermittedCrossDomainPolicies: 'none', ``` ## `X-XSS-Protection` @@ -250,10 +202,7 @@ Read more about this header [here](https://developer.mozilla.org/en-US/docs/Web/ Default value: ```ts -xXSSProtection: { - value: '0', - route: '/**', -}, +xXSSProtection: '0', ``` ## `Permissions-Policy` @@ -268,10 +217,7 @@ Default value: ```ts permissionsPolicy: { - value: { - 'camera': ['self'], - 'microphone': ['self'], - }, - route: '/**', + 'camera': ['self'], + 'microphone': ['self'], }, ``` diff --git a/docs/content/2.security/2.request-size-limiter.md b/docs/content/2.security/2.request-size-limiter.md index 55a07925..2f6dd08b 100644 --- a/docs/content/2.security/2.request-size-limiter.md +++ b/docs/content/2.security/2.request-size-limiter.md @@ -1,6 +1,6 @@ --- title: Request Size Limiter -description: '' +description: "" --- This middleware works for `GET`, `POST`, and `DELETE` methods and will throw an `413 Payload Too Large` error when the payload will be larger than the one set in the configuration. Works for both request size and upload file request size. @@ -20,13 +20,27 @@ To write a custom logic for this middleware follow this pattern: export default defineNuxtConfig({ security: { requestSizeLimiter: { - value: { - maxRequestSizeInBytes: 3000000, - maxUploadFileRequestInBytes: 9000000, - }, - route: '/my-custom-route' + maxRequestSizeInBytes: 3000000, + maxUploadFileRequestInBytes: 9000000, throwError: false, // optional } } }) ``` + +Or use `routeRules` for per route configuration: + +```js{}[nuxt.config.ts] +export default defineNuxtConfig({ + routeRules: { + '/my-secret-route': { + security: { + requestSizeLimiter: { + maxRequestSizeInBytes: 3000000, + maxUploadFileRequestInBytes: 9000000, + throwError: false, // optional + } + } + } + } +``` diff --git a/docs/content/2.security/3.rate-limiter.md b/docs/content/2.security/3.rate-limiter.md index 3b96c05a..7f74b969 100644 --- a/docs/content/2.security/3.rate-limiter.md +++ b/docs/content/2.security/3.rate-limiter.md @@ -21,14 +21,29 @@ To write a custom logic for this middleware follow this pattern: export default defineNuxtConfig({ security: { rateLimiter: { - value: { - tokensPerInterval: 200, - interval: "day", - fireImmediately: false - }, - route: '/my-custom-route', + tokensPerInterval: 200, + interval: "day", + fireImmediately: false throwError: false, // optional } } }) ``` + +Or use `routeRules` for per route configuration: + +```js{}[nuxt.config.ts] +export default defineNuxtConfig({ + routeRules: { + '/my-secret-route': { + security: { + rateLimiter: { + tokensPerInterval: 200, + interval: "day", + fireImmediately: false + throwError: false, // optional + } + } + } + } +``` diff --git a/docs/content/2.security/4.xss-validator.md b/docs/content/2.security/4.xss-validator.md index 172eea6e..b8ab5a84 100644 --- a/docs/content/2.security/4.xss-validator.md +++ b/docs/content/2.security/4.xss-validator.md @@ -22,12 +22,25 @@ To write a custom logic for this middleware follow this pattern: export default defineNuxtConfig({ security: { xssValidator: { - value: { - stripIgnoreTag: true - }, - route: '/my-custom-route', + stripIgnoreTag: true throwError: false, // optional } } }) ``` + +Or use `routeRules` for per route configuration: + +```js{}[nuxt.config.ts] +export default defineNuxtConfig({ + routeRules: { + '/my-secret-route': { + security: { + xssValidator: { + stripIgnoreTag: true + throwError: false, // optional + } + } + } + } +``` diff --git a/docs/content/2.security/5.cors-handler.md b/docs/content/2.security/5.cors-handler.md index bf154099..eb917484 100644 --- a/docs/content/2.security/5.cors-handler.md +++ b/docs/content/2.security/5.cors-handler.md @@ -27,12 +27,25 @@ To write a custom logic for this middleware follow this pattern: export default defineNuxtConfig({ security: { corsHandler: { - value: { - origin: '*', - methods: '*', - }, - route: '/my-custom-route' + origin: '*', + methods: '*', } } }) ``` + +Or use `routeRules` for per route configuration: + +```js{}[nuxt.config.ts] +export default defineNuxtConfig({ + routeRules: { + '/my-secret-route': { + security: { + corsHandler: { + origin: '*', + methods: '*', + } + } + } + } +``` diff --git a/docs/content/2.security/6.allowed-methods-restricter.md b/docs/content/2.security/6.allowed-methods-restricter.md index 7769c74c..445e6181 100644 --- a/docs/content/2.security/6.allowed-methods-restricter.md +++ b/docs/content/2.security/6.allowed-methods-restricter.md @@ -18,11 +18,20 @@ To write a custom logic for this middleware follow this pattern: ```js{}[nuxt.config.ts] export default defineNuxtConfig({ security: { - allowedMethodsRestricter: { - value: ['POST'], - route: '/my-custom-route', - throwError: false, // optional - } + allowedMethodsRestricter: ['POST'], } }) ``` + +Or use `routeRules` for per route configuration: + +```js{}[nuxt.config.ts] +export default defineNuxtConfig({ + routeRules: { + '/my-secret-route': { + security: { + allowedMethodsRestricter: ['POST'], + } + } + } +``` diff --git a/docs/content/2.security/7.basic-auth.md b/docs/content/2.security/7.basic-auth.md index 69eb801e..8df47276 100644 --- a/docs/content/2.security/7.basic-auth.md +++ b/docs/content/2.security/7.basic-auth.md @@ -22,13 +22,10 @@ To write a custom logic for this middleware follow this pattern: export default defineNuxtConfig({ security: { basicAuth: { - route: '/secret-route', - value: { - name: 'test', - pass: 'test', - enabled: true, - message: 'test' - } + name: 'test', + pass: 'test', + enabled: true, + message: 'test' } } }) diff --git a/playground/nuxt.config.ts b/playground/nuxt.config.ts index 41af2275..5d2b9a92 100644 --- a/playground/nuxt.config.ts +++ b/playground/nuxt.config.ts @@ -1,62 +1,32 @@ import { defineNuxtConfig } from 'nuxt/config' -import MyModule from '../src/module' +import NuxtSecurity from '../src/module' export default defineNuxtConfig({ - modules: [ - MyModule - ], - // security: { - // rateLimiter: { - // value: { - // tokensPerInterval: 2, - // interval: 'hour', - // fireImmediately: true - // }, - // route: '', - // throwError: false - // } - // } - // security: { - // basicAuth: { - // route: '', - // value: { - // name: 'test', - // pass: 'test', - // enabled: true, - // message: 'test' - // } - // } - // } - // security: { - // headers: { - // crossOriginResourcePolicy: { - // value: "test", - // route: '/**', - // }, - // }, - // requestSizeLimiter: { - // value: { - // maxRequestSizeInBytes: 3000000, - // maxUploadFileRequestInBytes: 9000000, - // }, - // route: '/upload-file' - // } - // } - // security: { - // headers: { - // contentSecurityPolicy: { - // value: { - // 'img-src': ["'self'", 'data:', 'https://dummy.test'] - // }, - // route: '/**' - // }, - // strictTransportSecurity: { - // value: { - // maxAge: 5552000, - // includeSubdomains: true - // }, - // route: '/**' - // } - // } - // } + modules: [NuxtSecurity], + + // Per route configuration + routeRules: { + 'secret': { + security: { + rateLimiter: { + tokensPerInterval: 2, + interval: 'hour', + } + }, + headers: { + xXSSProtection: '1' + }, + }, + }, + + // Global configuration + security: { + headers: { + xXSSProtection: '0' + }, + rateLimiter: { + tokensPerInterval: 3, + interval: 'day' + } + }, }) diff --git a/playground/pages/secret.vue b/playground/pages/secret.vue new file mode 100644 index 00000000..2cffe4e2 --- /dev/null +++ b/playground/pages/secret.vue @@ -0,0 +1,3 @@ + diff --git a/src/defaultConfig.ts b/src/defaultConfig.ts index fc6e6f4a..92452d6f 100644 --- a/src/defaultConfig.ts +++ b/src/defaultConfig.ts @@ -6,6 +6,18 @@ const defaultGlobalRoute = { route: DEFAULT_GLOBAL_ROUTE } const defaultMiddlewareRoute = { route: DEFAULT_MIDDLEWARE_ROUTE } const defaultThrowErrorValue = { throwError: true } +type SecurityMiddlewareNames = Record + +export const SECURITY_MIDDLEWARE_NAMES: SecurityMiddlewareNames = { + requestSizeLimiter: 'requestSizeLimiter', + rateLimiter: 'rateLimiter', + xssValidator: 'xssValidator', + corsHandler: 'corsHandler', + allowedMethodsRestricter: 'allowedMethodsRestricter', + basicAuth: 'basicAuth', + csrf: 'csrf' +} + export const defaultSecurityConfig = (serverlUrl: string): ModuleOptions => ({ headers: { crossOriginResourcePolicy: { diff --git a/src/module.ts b/src/module.ts index ecfd495e..ddd8bc52 100644 --- a/src/module.ts +++ b/src/module.ts @@ -1,181 +1,243 @@ -import { fileURLToPath } from 'node:url' -import { resolve, normalize } from 'pathe' -import { defineNuxtModule, addServerHandler, installModule } from '@nuxt/kit' -import defu, { createDefu } from 'defu' -import { RuntimeConfig } from '@nuxt/schema' +import { fileURLToPath } from "node:url"; +import { resolve, normalize } from "pathe"; +import { defineNuxtModule, addServerHandler, installModule } from "@nuxt/kit"; +import defu, { createDefu } from "defu"; +import { Nuxt, RuntimeConfig } from "@nuxt/schema"; import { AllowedHTTPMethods, BasicAuth, - CorsOptions, MiddlewareConfiguration, ModuleOptions, - RateLimiter, - RequestSizeLimiter, + NuxtSecurityRouteRules, SecurityHeaders, - XssValidator -} from './types' -import { defaultSecurityConfig } from './defaultConfig' -import { SECURITY_HEADER_NAMES, getHeaderValueFromOptions } from './headers' +} from "./types"; +import { + defaultSecurityConfig, + SECURITY_MIDDLEWARE_NAMES, +} from "./defaultConfig"; +import { SECURITY_HEADER_NAMES, getHeaderValueFromOptions } from "./headers"; -declare module '@nuxt/schema' { +declare module "@nuxt/schema" { interface NuxtOptions { security: ModuleOptions; } } -const defuReplaceArray = createDefu((obj, key, value) => { - if (Array.isArray(obj[key]) || Array.isArray(value)) { - obj[key] = value - return true +declare module "nitropack" { + interface NitroRouteRules { + security: NuxtSecurityRouteRules; } -}) + interface NitroRouteConfig { + security: NuxtSecurityRouteRules; + } +} export default defineNuxtModule({ meta: { - name: 'nuxt-security', - configKey: 'security' + name: "nuxt-security", + configKey: "security", }, - async setup (options, nuxt) { - // TODO: Migrate to createResolver (from @nuxt/kit) - const runtimeDir = fileURLToPath(new URL('./runtime', import.meta.url)) - nuxt.options.build.transpile.push(runtimeDir) - nuxt.options.security = defuReplaceArray({ ...options, ...nuxt.options.security }, { - ...defaultSecurityConfig(nuxt.options.devServer.url) - }) - const securityOptions = nuxt.options.security - // Disabled module when `enabled` is set to `false` - if (!securityOptions.enabled) return - - nuxt.hook('nitro:config', (config) => { - config.plugins = config.plugins || [] - - // Register nitro plugin to replace default 'X-Powered-By' header with custom one that does not indicate what is the framework underneath the app. - if (securityOptions.hidePoweredBy) { - config.externals = config.externals || {} - config.externals.inline = config.externals.inline || [] - config.externals.inline.push(normalize(fileURLToPath(new URL('./runtime', import.meta.url)))) - config.plugins.push( - normalize(fileURLToPath(new URL('./runtime/nitro/plugins/hidePoweredBy', import.meta.url))) - ) + async setup(options, nuxt) { + const runtimeDir = fileURLToPath(new URL("./runtime", import.meta.url)); + nuxt.options.build.transpile.push(runtimeDir); + nuxt.options.security = defuReplaceArray( + { ...options, ...nuxt.options.security }, + { + ...defaultSecurityConfig(nuxt.options.devServer.url), } + ); + const securityOptions = nuxt.options.security; + // Disabled module when `enabled` is set to `false` + if (!securityOptions.enabled) return; - // Register nitro plugin to enable CSP for SSG - if (typeof securityOptions.headers === 'object' && securityOptions.headers.contentSecurityPolicy) { - config.plugins.push( - normalize(fileURLToPath(new URL('./runtime/nitro/plugins/cspSsg', import.meta.url))) - ) - } - }) + registerSecurityNitroPlugins(nuxt, securityOptions); - nuxt.options.runtimeConfig.private = defu(nuxt.options.runtimeConfig.private, { - basicAuth: securityOptions.basicAuth as MiddlewareConfiguration - }) + nuxt.options.runtimeConfig.private = defu( + nuxt.options.runtimeConfig.private, + { + basicAuth: securityOptions.basicAuth as + | MiddlewareConfiguration + | BasicAuth + | boolean, + } + ); - delete (securityOptions as any).basicAuth + delete (securityOptions as any).basicAuth; - nuxt.options.runtimeConfig.security = defu(nuxt.options.runtimeConfig.security, { - ...securityOptions as RuntimeConfig['security'], - }) + nuxt.options.runtimeConfig.security = defu( + nuxt.options.runtimeConfig.security, + { + ...(securityOptions as RuntimeConfig["security"]), + } + ); - // Register enabled middlewares to automatically set default values for security response headers. if (securityOptions.headers) { - for (const header in securityOptions.headers as SecurityHeaders) { - if (securityOptions.headers[header as keyof typeof securityOptions.headers]) { - const nitroRouteRules = nuxt.options.nitro.routeRules - const headerOptions = securityOptions.headers[header as keyof typeof securityOptions.headers] - nitroRouteRules!![(headerOptions as any).route] = { - ...nitroRouteRules!![(headerOptions as any).route], - headers: { - ...nitroRouteRules!![(headerOptions as any).route]?.headers, - [SECURITY_HEADER_NAMES[header]]: getHeaderValueFromOptions(header as keyof SecurityHeaders, headerOptions as any) - } - } - } - } + setSecurityResponseHeaders(nuxt, securityOptions.headers); } - // Register requestSizeLimiter middleware with default values that will throw an error when the payload will be too big for methods like POST/PUT/DELETE. - const requestSizeLimiterConfig = nuxt.options.security.requestSizeLimiter - if (requestSizeLimiterConfig) { + setSecurityRouteRules(nuxt, securityOptions); + + if (nuxt.options.security.requestSizeLimiter) { addServerHandler({ - route: ( - requestSizeLimiterConfig as MiddlewareConfiguration - ).route, handler: normalize( - resolve(runtimeDir, 'server/middleware/requestSizeLimiter') - ) - }) + resolve(runtimeDir, "server/middleware/requestSizeLimiter") + ), + }); } - // Register rateLimiter middleware with default values that will throw an error when there will be too many requests from the same IP during certain interval. - // Based on 'limiter' package and stored in 'unstorage' for each ip address. - const rateLimiterConfig = securityOptions.rateLimiter - if (rateLimiterConfig) { + if (nuxt.options.security.rateLimiter) { addServerHandler({ - route: (rateLimiterConfig as MiddlewareConfiguration) - .route, handler: normalize( - resolve(runtimeDir, 'server/middleware/rateLimiter') - ) - }) + resolve(runtimeDir, "server/middleware/rateLimiter") + ), + }); } - // Register xssValidator middleware with default config that will return 400 Bad Request when either query or body will include unwanted characteds like diff --git a/test/fixtures/allowedMethods/nuxt.config.ts b/test/fixtures/allowedMethods/nuxt.config.ts new file mode 100644 index 00000000..011fa4f9 --- /dev/null +++ b/test/fixtures/allowedMethods/nuxt.config.ts @@ -0,0 +1,10 @@ +import MyModule from '../../../src/module' + +export default defineNuxtConfig({ + modules: [ + MyModule + ], + security: { + allowedMethodsRestricter: ['POST'] + } +}) diff --git a/test/fixtures/allowedMethods/package.json b/test/fixtures/allowedMethods/package.json new file mode 100644 index 00000000..decd4334 --- /dev/null +++ b/test/fixtures/allowedMethods/package.json @@ -0,0 +1,5 @@ +{ + "private": true, + "name": "basic", + "type": "module" +} diff --git a/test/fixtures/basic/app.vue b/test/fixtures/basic/app.vue new file mode 100644 index 00000000..29a9c81f --- /dev/null +++ b/test/fixtures/basic/app.vue @@ -0,0 +1,6 @@ + + + diff --git a/test/fixtures/basic/nuxt.config.ts b/test/fixtures/basic/nuxt.config.ts new file mode 100644 index 00000000..1bc2f7cc --- /dev/null +++ b/test/fixtures/basic/nuxt.config.ts @@ -0,0 +1,7 @@ +import MyModule from '../../../src/module' + +export default defineNuxtConfig({ + modules: [ + MyModule + ] +}) diff --git a/test/fixtures/basic/package.json b/test/fixtures/basic/package.json new file mode 100644 index 00000000..decd4334 --- /dev/null +++ b/test/fixtures/basic/package.json @@ -0,0 +1,5 @@ +{ + "private": true, + "name": "basic", + "type": "module" +} diff --git a/test/fixtures/basicAuth/app.vue b/test/fixtures/basicAuth/app.vue new file mode 100644 index 00000000..29a9c81f --- /dev/null +++ b/test/fixtures/basicAuth/app.vue @@ -0,0 +1,6 @@ + + + diff --git a/test/fixtures/basicAuth/nuxt.config.ts b/test/fixtures/basicAuth/nuxt.config.ts new file mode 100644 index 00000000..f1e88013 --- /dev/null +++ b/test/fixtures/basicAuth/nuxt.config.ts @@ -0,0 +1,15 @@ +import MyModule from '../../../src/module' + +export default defineNuxtConfig({ + modules: [ + MyModule + ], + security: { + basicAuth: { + name: 'test', + pass: 'test', + enabled: true, + message: 'test' + } + } +}) diff --git a/test/fixtures/basicAuth/package.json b/test/fixtures/basicAuth/package.json new file mode 100644 index 00000000..decd4334 --- /dev/null +++ b/test/fixtures/basicAuth/package.json @@ -0,0 +1,5 @@ +{ + "private": true, + "name": "basic", + "type": "module" +} diff --git a/test/fixtures/rateLimiter/app.vue b/test/fixtures/rateLimiter/app.vue new file mode 100644 index 00000000..29a9c81f --- /dev/null +++ b/test/fixtures/rateLimiter/app.vue @@ -0,0 +1,6 @@ + + + diff --git a/test/fixtures/rateLimiter/nuxt.config.ts b/test/fixtures/rateLimiter/nuxt.config.ts new file mode 100644 index 00000000..34fed450 --- /dev/null +++ b/test/fixtures/rateLimiter/nuxt.config.ts @@ -0,0 +1,14 @@ +import MyModule from '../../../src/module' + +export default defineNuxtConfig({ + modules: [ + MyModule + ], + security: { + rateLimiter: { + tokensPerInterval: 2, + interval: 'hour', + fireImmediately: true + } + } +}) diff --git a/test/fixtures/rateLimiter/package.json b/test/fixtures/rateLimiter/package.json new file mode 100644 index 00000000..decd4334 --- /dev/null +++ b/test/fixtures/rateLimiter/package.json @@ -0,0 +1,5 @@ +{ + "private": true, + "name": "basic", + "type": "module" +} diff --git a/test/fixtures/xss/app.vue b/test/fixtures/xss/app.vue new file mode 100644 index 00000000..29a9c81f --- /dev/null +++ b/test/fixtures/xss/app.vue @@ -0,0 +1,6 @@ + + + diff --git a/test/fixtures/xss/nuxt.config.ts b/test/fixtures/xss/nuxt.config.ts new file mode 100644 index 00000000..778b260a --- /dev/null +++ b/test/fixtures/xss/nuxt.config.ts @@ -0,0 +1,7 @@ +import MyModule from '../../../src/module' + +export default defineNuxtConfig({ + modules: [ + MyModule + ], +}) diff --git a/test/fixtures/xss/package.json b/test/fixtures/xss/package.json new file mode 100644 index 00000000..decd4334 --- /dev/null +++ b/test/fixtures/xss/package.json @@ -0,0 +1,5 @@ +{ + "private": true, + "name": "basic", + "type": "module" +} diff --git a/test/headers.test.ts b/test/headers.test.ts new file mode 100644 index 00000000..2abe6c42 --- /dev/null +++ b/test/headers.test.ts @@ -0,0 +1,172 @@ +import { describe, it, expect, beforeEach } from 'vitest' +import { fileURLToPath } from 'node:url' +import { setup, fetch } from '@nuxt/test-utils' + +describe('[nuxt-security] Headers', async () => { + await setup({ + rootDir: fileURLToPath(new URL('./fixtures/basic', import.meta.url)), + }) + let res + + it ('fetches the homepage', async () => { + res = await fetch('/') + + expect(res).toBeDefined() + expect(res).toBeTruthy() + }) + + + it('has `content-security-policy` header set with correct default value', async () => { + const { headers } = res + + expect(headers.has('content-security-policy')).toBeTruthy() + + const cspHeaderValue = headers.get('content-security-policy') + + expect(cspHeaderValue).toBeTruthy() + expect(cspHeaderValue).toBe("base-uri 'self'; 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'; upgrade-insecure-requests") + }) + + it('has `cross-origin-embedder-policy` header set with correct default value', async () => { + const { headers } = res + + expect(headers.has('cross-origin-embedder-policy')).toBeTruthy() + + const coepHeaderValue = headers.get('cross-origin-embedder-policy') + + expect(coepHeaderValue).toBeTruthy() + expect(coepHeaderValue).toBe('require-corp') + }) + + it('has `cross-origin-opener-policy` header set with correct default value', async () => { + const { headers } = res + + expect(headers.has('cross-origin-opener-policy')).toBeTruthy() + + const coopHeaderValue = headers.get('cross-origin-opener-policy') + + expect(coopHeaderValue).toBeTruthy() + expect(coopHeaderValue).toBe('same-origin') + }) + + it('has `cross-origin-resource-policy` header set with correct default value', async () => { + const { headers } = res + + expect(headers.has('cross-origin-resource-policy')).toBeTruthy() + + const corpHeaderValue = headers.get('cross-origin-resource-policy') + + expect(corpHeaderValue).toBeTruthy() + expect(corpHeaderValue).toBe('same-origin') + }) + + it('has `origin-agent-cluster` header set with correct default value', async () => { + const { headers } = res + + expect(headers.has('origin-agent-cluster')).toBeTruthy() + + const oacHeaderValue = headers.get('origin-agent-cluster') + + expect(oacHeaderValue).toBeTruthy() + expect(oacHeaderValue).toBe('?1') + }) + + it('has `permissions-policy` header set with correct default value', async () => { + const { headers } = res + + expect(headers.has('permissions-policy')).toBeTruthy() + + const ppHeaderValue = headers.get('permissions-policy') + + expect(ppHeaderValue).toBeTruthy() + expect(ppHeaderValue).toBe('camera=(), display-capture=(), fullscreen=(), geolocation=(), microphone=()') + }) + + it('has `referrer-policy` header set with correct default value', async () => { + const { headers } = res + + expect(headers.has('referrer-policy')).toBeTruthy() + + const rpHeaderValue = headers.get('referrer-policy') + + expect(rpHeaderValue).toBeTruthy() + expect(rpHeaderValue).toBe('no-referrer') + }) + + it('has `strict-transport-security` header set with correct default value', async () => { + const { headers } = res + + expect(headers.has('strict-transport-security')).toBeTruthy() + + const stsHeaderValue = headers.get('strict-transport-security') + + expect(stsHeaderValue).toBeTruthy() + expect(stsHeaderValue).toBe('max-age=15552000; includeSubDomains') + }) + + it('has `x-content-type-options` header set with correct default value', async () => { + const { headers } = res + + expect(headers.has('x-content-type-options')).toBeTruthy() + + const xctpHeaderValue = headers.get('x-content-type-options') + + expect(xctpHeaderValue).toBeTruthy() + expect(xctpHeaderValue).toBe('nosniff') + }) + + it('has `x-dns-prefetch-control` header set with correct default value', async () => { + const { headers } = res + + expect(headers.has('x-dns-prefetch-control')).toBeTruthy() + + const xdpcHeaderValue = headers.get('x-dns-prefetch-control') + + expect(xdpcHeaderValue).toBeTruthy() + expect(xdpcHeaderValue).toBe('off') + }) + + it('has `x-download-options` header set with correct default value', async () => { + const { headers } = res + + expect(headers.has('x-download-options')).toBeTruthy() + + const xdoHeaderValue = headers.get('x-download-options') + + expect(xdoHeaderValue).toBeTruthy() + expect(xdoHeaderValue).toBe('noopen') + }) + + it('has `x-frame-options` header set with correct default value', async () => { + const { headers } = res + + expect(headers.has('x-frame-options')).toBeTruthy() + + const xfoHeaderValue = headers.get('x-frame-options') + + expect(xfoHeaderValue).toBeTruthy() + expect(xfoHeaderValue).toBe('SAMEORIGIN') + }) + + it('has `x-permitted-cross-domain-policies` header set with correct default value', async () => { + const { headers } = res + + expect(headers.has('x-permitted-cross-domain-policies')).toBeTruthy() + + const xpcdpHeaderValue = headers.get('x-permitted-cross-domain-policies') + + expect(xpcdpHeaderValue).toBeTruthy() + expect(xpcdpHeaderValue).toBe('none') + }) + + it('has `x-xss-protection` header set with correct default value', async () => { + const { headers } = res + + expect(headers.has('x-xss-protection')).toBeTruthy() + + const xxpHeaderValue = headers.get('x-xss-protection') + + expect(xxpHeaderValue).toBeTruthy() + expect(xxpHeaderValue).toBe('0') + }) +}) diff --git a/test/rateLimiter.test.ts b/test/rateLimiter.test.ts new file mode 100644 index 00000000..29bdcd0e --- /dev/null +++ b/test/rateLimiter.test.ts @@ -0,0 +1,32 @@ +import { describe, it, expect } from 'vitest' +import { fileURLToPath } from 'node:url' +import { setup, fetch } from '@nuxt/test-utils' + +describe('[nuxt-security] Rate Limiter', async () => { + await setup({ + rootDir: fileURLToPath(new URL('./fixtures/rateLimiter', import.meta.url)), + }) + + it ('should return 200 OK when not reaching the limit', async () => { + const res1 = await fetch('/') + const res2 = await fetch('/') + + expect(res1).toBeDefined() + expect(res1).toBeTruthy() + expect(res2.status).toBe(200) + expect(res2.statusText).toBe('OK') + }) + + it ('should return 429 Too Many Responses after limit reached', async () => { + const res1 = await fetch('/') + const res2 = await fetch('/') + const res3 = await fetch('/') + const res4 = await fetch('/') + const res5 = await fetch('/') + + expect(res1).toBeDefined() + expect(res1).toBeTruthy() + expect(res5.status).toBe(429) + expect(res5.statusText).toBe('Too Many Requests') + }) +}) diff --git a/test/xssValidator.test.ts b/test/xssValidator.test.ts new file mode 100644 index 00000000..760306a6 --- /dev/null +++ b/test/xssValidator.test.ts @@ -0,0 +1,16 @@ +import { describe, it, expect } from 'vitest' +import { fileURLToPath } from 'node:url' +import { setup, fetch } from '@nuxt/test-utils' + +describe('[nuxt-security] Cross Site Scripting', async () => { + await setup({ + rootDir: fileURLToPath(new URL('./fixtures/xss', import.meta.url)), + }) + + it ('should return 400 Bad request when passing a script in query or body', async () => { + const res = await fetch('/?test=') + + expect(res.status).toBe(400) + expect(res.statusText).toBe('Bad Request') + }) +}) diff --git a/yarn.lock b/yarn.lock index bf3ab1a8..5f038f8f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -790,6 +790,30 @@ unimport "^2.2.4" untyped "^1.2.2" +"@nuxt/kit@3.3.1": + version "3.3.1" + resolved "https://registry.yarnpkg.com/@nuxt/kit/-/kit-3.3.1.tgz#93e02a41d9adc7369d704deb88ae143c29554a4e" + integrity sha512-zb7/2FUIB1g7nl6K6qozUzfG5uu4yrs9TQjZvpASnPBZ/x1EuJX5k3AA71hMMIVBEX9Adxvh9AuhDEHE5W26Zg== + dependencies: + "@nuxt/schema" "3.3.1" + c12 "^1.2.0" + consola "^2.15.3" + defu "^6.1.2" + globby "^13.1.3" + hash-sum "^2.0.0" + ignore "^5.2.4" + jiti "^1.17.2" + knitwork "^1.0.0" + lodash.template "^4.5.0" + mlly "^1.2.0" + pathe "^1.1.0" + pkg-types "^1.0.2" + scule "^1.0.0" + semver "^7.3.8" + unctx "^2.1.2" + unimport "^3.0.2" + untyped "^1.2.2" + "@nuxt/kit@^3.2.0": version "3.2.0" resolved "https://registry.npmjs.org/@nuxt/kit/-/kit-3.2.0.tgz" @@ -863,6 +887,25 @@ unimport "^2.2.4" untyped "^1.2.2" +"@nuxt/schema@3.3.1": + version "3.3.1" + resolved "https://registry.yarnpkg.com/@nuxt/schema/-/schema-3.3.1.tgz#305d0db9c535f64fad3aa1a7c985410e733f9ff0" + integrity sha512-E8HWzU43rXzqwDTmWduTLHY4xIwRSAUt1LbpuE9IjZ4uJZq5Mbaj4nfhANNsTQGw2c+O+rL81yzAP3i61LEJDw== + dependencies: + c12 "^1.2.0" + create-require "^1.1.1" + defu "^6.1.2" + hookable "^5.5.0" + jiti "^1.17.2" + pathe "^1.1.0" + pkg-types "^1.0.2" + postcss-import-resolver "^2.0.0" + scule "^1.0.0" + std-env "^3.3.2" + ufo "^1.1.1" + unimport "^3.0.2" + untyped "^1.2.2" + "@nuxt/telemetry@^2.1.10": version "2.1.10" resolved "https://registry.npmjs.org/@nuxt/telemetry/-/telemetry-2.1.10.tgz" @@ -889,6 +932,21 @@ rc9 "^2.0.1" std-env "^3.3.2" +"@nuxt/test-utils@^3.2.2": + version "3.3.1" + resolved "https://registry.yarnpkg.com/@nuxt/test-utils/-/test-utils-3.3.1.tgz#364d07074b44f328c58032e7eac8e476e9c31b28" + integrity sha512-kkX5OTQ43Gc8CltSPlvem55rKfg1tOSq9FWLTu+Z1z00wc1oQzDAdVoHz0uSKT9anmbaWMX2u6c0oJb1DukyCg== + dependencies: + "@nuxt/kit" "3.3.1" + "@nuxt/schema" "3.3.1" + consola "^2.15.3" + defu "^6.1.2" + execa "^7.1.0" + get-port-please "^3.0.1" + jiti "^1.17.2" + ofetch "^1.0.1" + pathe "^1.1.0" + "@nuxt/ui-templates@^1.1.1": version "1.1.1" resolved "https://registry.npmjs.org/@nuxt/ui-templates/-/ui-templates-1.1.1.tgz" @@ -1102,6 +1160,18 @@ resolved "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz" integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA== +"@types/chai-subset@^1.3.3": + version "1.3.3" + resolved "https://registry.yarnpkg.com/@types/chai-subset/-/chai-subset-1.3.3.tgz#97893814e92abd2c534de422cb377e0e0bdaac94" + integrity sha512-frBecisrNGz+F4T6bcc+NLeolfiojh5FxW2klu669+8BARtyQv2C/GkNW6FUodVe4BroGMP/wER/YDGc7rEllw== + dependencies: + "@types/chai" "*" + +"@types/chai@*", "@types/chai@^4.3.4": + version "4.3.4" + resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.4.tgz#e913e8175db8307d78b4e8fa690408ba6b65dee4" + integrity sha512-KnRanxnpfpjUTqTCXslZSEdLfXExwgNxYPdiO2WGUj8+HDjFi8R3k5RVKPeSCzLjCcshCAtVO2QBbVuAV4kTnw== + "@types/estree@*", "@types/estree@^1.0.0": version "1.0.0" resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.0.tgz" @@ -1122,6 +1192,11 @@ resolved "https://registry.npmjs.org/@types/memory-cache/-/memory-cache-0.2.2.tgz" integrity sha512-xNnm6EkmYYhTnLiOHC2bdKgcYY5qjjrq5vl9KXD2nh0em0koZoFS500EL4Q4V/eW+A3P7NC7P7GIYzNOSQp7jQ== +"@types/node@*": + version "18.15.3" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.3.tgz#f0b991c32cfc6a4e7f3399d6cb4b8cf9a0315014" + integrity sha512-p6ua9zBxz5otCmbpb5D3U4B5Nanw6Pk3PPyX05xnxbB/fRv71N7CPmORg7uAD5P70T0xmx1pzAx/FUfa5X+3cw== + "@types/node@^18.14.4": version "18.14.4" resolved "https://registry.npmjs.org/@types/node/-/node-18.14.4.tgz" @@ -1297,6 +1372,42 @@ resolved "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.0.0.tgz" integrity sha512-e0X4jErIxAB5oLtDqbHvHpJe/uWNkdpYV83AOG2xo2tEVSzCzewgJMtREZM30wXnM5ls90hxiOtAuVU6H5JgbA== +"@vitest/expect@0.28.5": + version "0.28.5" + resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-0.28.5.tgz#d5a6eccd014e9ad66fe87a20d16426a2815c0e8a" + integrity sha512-gqTZwoUTwepwGIatnw4UKpQfnoyV0Z9Czn9+Lo2/jLIt4/AXLTn+oVZxlQ7Ng8bzcNkR+3DqLJ08kNr8jRmdNQ== + dependencies: + "@vitest/spy" "0.28.5" + "@vitest/utils" "0.28.5" + chai "^4.3.7" + +"@vitest/runner@0.28.5": + version "0.28.5" + resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-0.28.5.tgz#4a18fe0e40b25569763f9f1f64b799d1629b3026" + integrity sha512-NKkHtLB+FGjpp5KmneQjTcPLWPTDfB7ie+MmF1PnUBf/tGe2OjGxWyB62ySYZ25EYp9krR5Bw0YPLS/VWh1QiA== + dependencies: + "@vitest/utils" "0.28.5" + p-limit "^4.0.0" + pathe "^1.1.0" + +"@vitest/spy@0.28.5": + version "0.28.5" + resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-0.28.5.tgz#b69affa0786200251b9e5aac5c58bbfb1b3273c9" + integrity sha512-7if6rsHQr9zbmvxN7h+gGh2L9eIIErgf8nSKYDlg07HHimCxp4H6I/X/DPXktVPPLQfiZ1Cw2cbDIx9fSqDjGw== + dependencies: + tinyspy "^1.0.2" + +"@vitest/utils@0.28.5": + version "0.28.5" + resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-0.28.5.tgz#7b82b528df86adfbd4a1f6a3b72c39790e81de0d" + integrity sha512-UyZdYwdULlOa4LTUSwZ+Paz7nBHGTT72jKwdFSV4IjHF1xsokp+CabMdhjvVhYwkLfO88ylJT46YMilnkSARZA== + dependencies: + cli-truncate "^3.1.0" + diff "^5.1.0" + loupe "^2.3.6" + picocolors "^1.0.0" + pretty-format "^27.5.1" + "@vue/babel-helper-vue-transform-on@^1.0.2": version "1.0.2" resolved "https://registry.npmjs.org/@vue/babel-helper-vue-transform-on/-/babel-helper-vue-transform-on-1.0.2.tgz" @@ -1432,6 +1543,11 @@ acorn-jsx@^5.3.2: resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== +acorn-walk@^8.2.0: + version "8.2.0" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" + integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== + acorn@^8.5.0, acorn@^8.6.0, acorn@^8.8.0, acorn@^8.8.1: version "8.8.1" resolved "https://registry.npmjs.org/acorn/-/acorn-8.8.1.tgz" @@ -1497,7 +1613,12 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0: dependencies: color-convert "^2.0.1" -ansi-styles@^6.1.0: +ansi-styles@^5.0.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" + integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== + +ansi-styles@^6.0.0, ansi-styles@^6.1.0: version "6.2.1" resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz" integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== @@ -1596,6 +1717,11 @@ array.prototype.flat@^1.2.5: es-abstract "^1.20.4" es-shim-unscopables "^1.0.0" +assertion-error@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b" + integrity sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw== + async-sema@^3.1.1: version "3.1.1" resolved "https://registry.npmjs.org/async-sema/-/async-sema-3.1.1.tgz" @@ -1754,6 +1880,20 @@ c12@^1.1.0, c12@^1.1.2: pkg-types "^1.0.2" rc9 "^2.0.1" +c12@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/c12/-/c12-1.2.0.tgz#2c946dd5ea37fd4f21c4b2671bf21f69a7028707" + integrity sha512-CMznkE0LpNEuD8ILp5QvsQVP+YvcpJnrI/zFeFnosU2PyDtx1wT7tXfZ8S3Tl3l9MTTXbKeuhDYKwgvnAPOx3w== + dependencies: + defu "^6.1.2" + dotenv "^16.0.3" + giget "^1.1.2" + jiti "^1.17.2" + mlly "^1.2.0" + pathe "^1.1.0" + pkg-types "^1.0.2" + rc9 "^2.0.1" + cac@^6.7.14: version "6.7.14" resolved "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz" @@ -1792,6 +1932,19 @@ caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001400, caniuse-lite@^1.0.30001426: resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001434.tgz" integrity sha512-aOBHrLmTQw//WFa2rcF1If9fa3ypkC1wzqqiKHgfdrXTWcU8C4gKVZT77eQAPWN1APys3+uQ0Df07rKauXGEYA== +chai@^4.3.7: + version "4.3.7" + resolved "https://registry.yarnpkg.com/chai/-/chai-4.3.7.tgz#ec63f6df01829088e8bf55fca839bcd464a8ec51" + integrity sha512-HLnAzZ2iupm25PlN0xFreAlBA5zaBSv3og0DdeGA4Ar6h6rJ3A0rolRUKJhSF2V10GZKDgWF/VmAEsNWjCRB+A== + dependencies: + assertion-error "^1.1.0" + check-error "^1.0.2" + deep-eql "^4.1.2" + get-func-name "^2.0.0" + loupe "^2.3.1" + pathval "^1.1.1" + type-detect "^4.0.5" + chalk@^2.0.0: version "2.4.2" resolved "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz" @@ -1824,6 +1977,11 @@ chardet@^0.7.0: resolved "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz" integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== +check-error@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82" + integrity sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA== + chokidar@^3.5.1, chokidar@^3.5.3: version "3.5.3" resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz" @@ -1873,6 +2031,14 @@ cli-spinners@^2.6.1: resolved "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.7.0.tgz" integrity sha512-qu3pN8Y3qHNgE2AFweciB1IfMnmZ/fsNTEE+NOFjmGB2F/7rLhnhzppvpCnN4FovtP26k8lHyy9ptEbNwWFLzw== +cli-truncate@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-3.1.0.tgz#3f23ab12535e3d73e839bb43e73c9de487db1389" + integrity sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA== + dependencies: + slice-ansi "^5.0.0" + string-width "^5.0.0" + cli-width@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/cli-width/-/cli-width-4.0.0.tgz" @@ -2163,6 +2329,13 @@ debug@^3.2.7: dependencies: ms "^2.1.1" +deep-eql@^4.1.2: + version "4.1.3" + resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-4.1.3.tgz#7c7775513092f7df98d8df9996dd085eb668cc6d" + integrity sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw== + dependencies: + type-detect "^4.0.0" + deep-is@^0.1.3: version "0.1.4" resolved "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz" @@ -2233,6 +2406,11 @@ detect-libc@^2.0.0: resolved "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz" integrity sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w== +diff@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-5.1.0.tgz#bc52d298c5ea8df9194800224445ed43ffc87e40" + integrity sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw== + dir-glob@^3.0.1: version "3.0.1" resolved "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz" @@ -2927,6 +3105,21 @@ execa@^5.1.1: signal-exit "^3.0.3" strip-final-newline "^2.0.0" +execa@^7.1.0: + version "7.1.1" + resolved "https://registry.yarnpkg.com/execa/-/execa-7.1.1.tgz#3eb3c83d239488e7b409d48e8813b76bb55c9c43" + integrity sha512-wH0eMf/UXckdUYnO21+HDztteVv05rq2GXksxT4fCGeHkBhw1DROXh40wcjMcRqDOWE7iPJ4n3M7e2+YFP+76Q== + dependencies: + cross-spawn "^7.0.3" + get-stream "^6.0.1" + human-signals "^4.3.0" + is-stream "^3.0.0" + merge-stream "^2.0.0" + npm-run-path "^5.1.0" + onetime "^6.0.0" + signal-exit "^3.0.7" + strip-final-newline "^3.0.0" + external-editor@^3.0.3: version "3.1.0" resolved "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz" @@ -3155,6 +3348,11 @@ get-caller-file@^2.0.5: resolved "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== +get-func-name@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.0.tgz#ead774abee72e20409433a066366023dd6887a41" + integrity sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig== + get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3: version "1.1.3" resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz" @@ -3169,9 +3367,9 @@ get-port-please@^3.0.1: resolved "https://registry.npmjs.org/get-port-please/-/get-port-please-3.0.1.tgz" integrity sha512-R5pcVO8Z1+pVDu8Ml3xaJCEkBiiy1VQN9za0YqH8GIi1nIqD4IzQhzY6dDzMRtdS1lyiGlucRzm8IN8wtLIXng== -get-stream@^6.0.0: +get-stream@^6.0.0, get-stream@^6.0.1: version "6.0.1" - resolved "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== get-symbol-description@^1.0.0: @@ -3200,6 +3398,19 @@ giget@^1.1.0: pathe "^1.1.0" tar "^6.1.13" +giget@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/giget/-/giget-1.1.2.tgz#f99a49cb0ff85479c8c3612cdc7ca27f2066e818" + integrity sha512-HsLoS07HiQ5oqvObOI+Qb2tyZH4Gj5nYGfF9qQcZNrPw+uEFhdXtgJr01aO2pWadGHucajYDLxxbtQkm97ON2A== + dependencies: + colorette "^2.0.19" + defu "^6.1.2" + https-proxy-agent "^5.0.1" + mri "^1.2.0" + node-fetch-native "^1.0.2" + pathe "^1.1.0" + tar "^6.1.13" + git-config-path@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/git-config-path/-/git-config-path-2.0.0.tgz" @@ -3399,6 +3610,11 @@ hookable@^5.4.2: resolved "https://registry.npmjs.org/hookable/-/hookable-5.4.2.tgz" integrity sha512-6rOvaUiNKy9lET1X0ECnyZ5O5kSV0PJbtA5yZUgdEF7fGJEVwSLSislltyt7nFwVVALYHQJtfGeAR2Y0A0uJkg== +hookable@^5.5.0: + version "5.5.1" + resolved "https://registry.yarnpkg.com/hookable/-/hookable-5.5.1.tgz#3a842c66be72a9cb14043d8e6afa5bc094c831c5" + integrity sha512-ac50aYjbtRMMZEtTG0qnVaBDA+1lqL9fHzDnxMQlVuO6LZWcBB7NXjIu9H9iImClewNdrit4RiEzi9QpRTgKrg== + hosted-git-info@^2.1.4: version "2.8.9" resolved "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz" @@ -3447,6 +3663,11 @@ human-signals@^2.1.0: resolved "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz" integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== +human-signals@^4.3.0: + version "4.3.1" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-4.3.1.tgz#ab7f811e851fca97ffbd2c1fe9a958964de321b2" + integrity sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ== + iconv-lite@^0.4.24: version "0.4.24" resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz" @@ -3633,6 +3854,11 @@ is-fullwidth-code-point@^3.0.0: resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz" integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== +is-fullwidth-code-point@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz#fae3167c729e7463f8461ce512b080a49268aa88" + integrity sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ== + is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: version "4.0.3" resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz" @@ -3716,6 +3942,11 @@ is-stream@^2.0.0: resolved "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz" integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== +is-stream@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-3.0.0.tgz#e6bfd7aa6bef69f4f472ce9bb681e3e57b4319ac" + integrity sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA== + is-string@^1.0.5, is-string@^1.0.7: version "1.0.7" resolved "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz" @@ -3769,6 +4000,11 @@ jiti@^1.17.0, jiti@^1.17.1: resolved "https://registry.npmjs.org/jiti/-/jiti-1.17.1.tgz" integrity sha512-NZIITw8uZQFuzQimqjUxIrIcEdxYDFIe/0xYfIlVXTkiBjjyBEvgasj5bb0/cHtPRD/NziPbT312sFrkI5ALpw== +jiti@^1.17.2: + version "1.18.2" + resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.18.2.tgz#80c3ef3d486ebf2450d9335122b32d121f2a83cd" + integrity sha512-QAdOptna2NYiSSpv0O/BwoHBSmz4YhpzJHyi+fnMRTXFjp7B8i/YG5Z8IfusxB1ufjcD2Sre1F3R+nX3fvy7gg== + js-sdsl@^4.1.4: version "4.2.0" resolved "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.2.0.tgz" @@ -4010,6 +4246,13 @@ log-symbols@^5.1.0: chalk "^5.0.0" is-unicode-supported "^1.1.0" +loupe@^2.3.1, loupe@^2.3.6: + version "2.3.6" + resolved "https://registry.yarnpkg.com/loupe/-/loupe-2.3.6.tgz#76e4af498103c532d1ecc9be102036a21f787b53" + integrity sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA== + dependencies: + get-func-name "^2.0.0" + lru-cache@^5.1.1: version "5.1.1" resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz" @@ -4120,6 +4363,11 @@ mimic-fn@^2.1.0: resolved "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== +mimic-fn@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-4.0.0.tgz#60a90550d5cb0b239cca65d893b1a53b29871ecc" + integrity sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw== + min-indent@^1.0.0: version "1.0.1" resolved "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz" @@ -4204,6 +4452,16 @@ mlly@^1.0.0, mlly@^1.1.0, mlly@^1.1.1: pkg-types "^1.0.1" ufo "^1.1.0" +mlly@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/mlly/-/mlly-1.2.0.tgz#f0f6c2fc8d2d12ea6907cd869066689b5031b613" + integrity sha512-+c7A3CV0KGdKcylsI6khWyts/CYrGTrRVo4R/I7u/cUsy0Conxa6LUhiEzVKIw14lc2L5aiO4+SeVe4TeGRKww== + dependencies: + acorn "^8.8.2" + pathe "^1.1.0" + pkg-types "^1.0.2" + ufo "^1.1.1" + mri@^1.2.0: version "1.2.0" resolved "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz" @@ -4393,6 +4651,13 @@ npm-run-path@^4.0.1: dependencies: path-key "^3.0.0" +npm-run-path@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-5.1.0.tgz#bc62f7f3f6952d9894bd08944ba011a6ee7b7e00" + integrity sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q== + dependencies: + path-key "^4.0.0" + npmlog@^5.0.1: version "5.0.1" resolved "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz" @@ -4544,6 +4809,13 @@ onetime@^5.1.0, onetime@^5.1.2: dependencies: mimic-fn "^2.1.0" +onetime@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-6.0.0.tgz#7c24c18ed1fd2e9bca4bd26806a33613c77d34b4" + integrity sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ== + dependencies: + mimic-fn "^4.0.0" + open@^8.4.0: version "8.4.0" resolved "https://registry.npmjs.org/open/-/open-8.4.0.tgz" @@ -4599,6 +4871,13 @@ p-limit@^3.0.2: dependencies: yocto-queue "^0.1.0" +p-limit@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-4.0.0.tgz#914af6544ed32bfa54670b061cafcbd04984b644" + integrity sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ== + dependencies: + yocto-queue "^1.0.0" + p-locate@^4.1.0: version "4.1.0" resolved "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz" @@ -4677,6 +4956,11 @@ path-key@^3.0.0, path-key@^3.1.0: resolved "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz" integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== +path-key@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-4.0.0.tgz#295588dc3aee64154f877adb9d780b81c554bf18" + integrity sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ== + path-parse@^1.0.7: version "1.0.7" resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz" @@ -4697,6 +4981,11 @@ pathe@^1.1.0: resolved "https://registry.npmjs.org/pathe/-/pathe-1.1.0.tgz" integrity sha512-ODbEPR0KKHqECXW1GoxdDb+AZvULmXjVPy4rt+pGo2+TnjJTIPJQSVS6N63n8T2Ip+syHhbn52OewKicV0373w== +pathval@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.1.tgz#8534e77a77ce7ac5a2512ea21e0fdb8fcf6c3d8d" + integrity sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ== + perfect-debounce@^0.1.3: version "0.1.3" resolved "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-0.1.3.tgz" @@ -5009,6 +5298,15 @@ pretty-bytes@^6.1.0: resolved "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.1.0.tgz" integrity sha512-Rk753HI8f4uivXi4ZCIYdhmG1V+WKzvRMg/X+M42a6t7D07RcmopXJMDNk6N++7Bl75URRGsb40ruvg7Hcp2wQ== +pretty-format@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.5.1.tgz#2181879fdea51a7a5851fb39d920faa63f01d88e" + integrity sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ== + dependencies: + ansi-regex "^5.0.1" + ansi-styles "^5.0.0" + react-is "^17.0.1" + process-nextick-args@~2.0.0: version "2.0.1" resolved "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz" @@ -5060,6 +5358,11 @@ rc9@^2.0.1: destr "^1.2.2" flat "^5.0.2" +react-is@^17.0.1: + version "17.0.2" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" + integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== + read-cache@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz" @@ -5388,9 +5691,14 @@ side-channel@^1.0.4: get-intrinsic "^1.0.2" object-inspect "^1.9.0" -signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3: +siginfo@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/siginfo/-/siginfo-2.0.0.tgz#32e76c70b79724e3bb567cb9d543eb858ccfaf30" + integrity sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g== + +signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7: version "3.0.7" - resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== slash@^3.0.0: @@ -5403,6 +5711,14 @@ slash@^4.0.0: resolved "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz" integrity sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew== +slice-ansi@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-5.0.0.tgz#b73063c57aa96f9cd881654b15294d95d285c42a" + integrity sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ== + dependencies: + ansi-styles "^6.0.0" + is-fullwidth-code-point "^4.0.0" + smob@^0.0.6: version "0.0.6" resolved "https://registry.npmjs.org/smob/-/smob-0.0.6.tgz" @@ -5467,6 +5783,11 @@ stable@^0.1.8: resolved "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz" integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w== +stackback@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/stackback/-/stackback-0.0.2.tgz#1ac8a0d9483848d1695e418b6d031a3c3ce68e3b" + integrity sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw== + standard-as-callback@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz" @@ -5477,7 +5798,7 @@ statuses@2.0.1: resolved "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz" integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== -std-env@^3.3.2: +std-env@^3.3.1, std-env@^3.3.2: version "3.3.2" resolved "https://registry.npmjs.org/std-env/-/std-env-3.3.2.tgz" integrity sha512-uUZI65yrV2Qva5gqE0+A7uVAvO40iPo6jGhs7s8keRfHCmtg+uB2X6EiLGCI9IgL1J17xGhvoOqSz79lzICPTA== @@ -5491,7 +5812,7 @@ std-env@^3.3.2: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.1" -string-width@^5.0.1, string-width@^5.1.2: +string-width@^5.0.0, string-width@^5.0.1, string-width@^5.1.2: version "5.1.2" resolved "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz" integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== @@ -5556,6 +5877,11 @@ strip-final-newline@^2.0.0: resolved "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz" integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== +strip-final-newline@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-3.0.0.tgz#52894c313fbff318835280aed60ff71ebf12b8fd" + integrity sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw== + strip-indent@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz" @@ -5713,6 +6039,21 @@ tiny-invariant@^1.1.0: resolved "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.1.tgz" integrity sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw== +tinybench@^2.3.1: + version "2.4.0" + resolved "https://registry.yarnpkg.com/tinybench/-/tinybench-2.4.0.tgz#83f60d9e5545353610fe7993bd783120bc20c7a7" + integrity sha512-iyziEiyFxX4kyxSp+MtY1oCH/lvjH3PxFN8PGCDeqcZWAJ/i+9y+nL85w99PxVzrIvew/GSkSbDYtiGVa85Afg== + +tinypool@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-0.3.1.tgz#a99c2e446aba9be05d3e1cb756d6aed7af4723b6" + integrity sha512-zLA1ZXlstbU2rlpA4CIeVaqvWq41MTWqLY3FfsAXgC8+f7Pk7zroaJQxDgxn1xNudKW6Kmj4808rPFShUlIRmQ== + +tinyspy@^1.0.2: + version "1.1.1" + resolved "https://registry.yarnpkg.com/tinyspy/-/tinyspy-1.1.1.tgz#0cb91d5157892af38cb2d217f5c7e8507a5bf092" + integrity sha512-UVq5AXt/gQlti7oxoIg5oi/9r0WpF7DGEVwXgqWSMmyN16+e3tl5lIvTaOpJ3TAtu5xFzWccFRM4R5NaWHF+4g== + tmp@^0.0.33: version "0.0.33" resolved "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz" @@ -5776,6 +6117,11 @@ type-check@^0.4.0, type-check@~0.4.0: dependencies: prelude-ls "^1.2.1" +type-detect@^4.0.0, type-detect@^4.0.5: + version "4.0.8" + resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" + integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== + type-fest@^0.20.2: version "0.20.2" resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz" @@ -5921,6 +6267,23 @@ unimport@^2.2.4: strip-literal "^1.0.0" unplugin "^1.0.1" +unimport@^3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/unimport/-/unimport-3.0.3.tgz#f5750a934086310d1595384899ff09404cc3dfd1" + integrity sha512-RzQqQiqepF5P13SwBGCe4pLlRnAQlbFuDAaQlSkXiNJDpN2iymtGMSfa75AcVSejgV05Q2aQYt6UhCiy5GuZ2A== + dependencies: + "@rollup/pluginutils" "^5.0.2" + escape-string-regexp "^5.0.0" + fast-glob "^3.2.12" + local-pkg "^0.4.3" + magic-string "^0.30.0" + mlly "^1.2.0" + pathe "^1.1.0" + pkg-types "^1.0.2" + scule "^1.0.0" + strip-literal "^1.0.1" + unplugin "^1.3.1" + universalify@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz" @@ -5936,6 +6299,16 @@ unplugin@^1.0.1, unplugin@^1.1.0: webpack-sources "^3.2.3" webpack-virtual-modules "^0.5.0" +unplugin@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/unplugin/-/unplugin-1.3.1.tgz#7af993ba8695d17d61b0845718380caf6af5109f" + integrity sha512-h4uUTIvFBQRxUKS2Wjys6ivoeofGhxzTe2sRWlooyjHXVttcVfV/JiavNd3d4+jty0SVV0dxGw9AkY9MwiaCEw== + dependencies: + acorn "^8.8.2" + chokidar "^3.5.3" + webpack-sources "^3.2.3" + webpack-virtual-modules "^0.5.0" + unstorage@^1.1.5: version "1.1.5" resolved "https://registry.npmjs.org/unstorage/-/unstorage-1.1.5.tgz" @@ -6005,6 +6378,20 @@ validate-npm-package-license@^3.0.1: spdx-correct "^3.0.0" spdx-expression-parse "^3.0.0" +vite-node@0.28.5: + version "0.28.5" + resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-0.28.5.tgz#56d0f78846ea40fddf2e28390899df52a4738006" + integrity sha512-LmXb9saMGlrMZbXTvOveJKwMTBTNUH66c8rJnQ0ZPNX+myPEol64+szRzXtV5ORb0Hb/91yq+/D3oERoyAt6LA== + dependencies: + cac "^6.7.14" + debug "^4.3.4" + mlly "^1.1.0" + pathe "^1.1.0" + picocolors "^1.0.0" + source-map "^0.6.1" + source-map-support "^0.5.21" + vite "^3.0.0 || ^4.0.0" + vite-node@^0.29.1: version "0.29.2" resolved "https://registry.npmjs.org/vite-node/-/vite-node-0.29.2.tgz" @@ -6063,6 +6450,36 @@ vite@~4.1.4: optionalDependencies: fsevents "~2.3.2" +vitest@^0.28.5: + version "0.28.5" + resolved "https://registry.yarnpkg.com/vitest/-/vitest-0.28.5.tgz#94410a8924cd7189e4f1adffa8c5cde809cbf2f9" + integrity sha512-pyCQ+wcAOX7mKMcBNkzDwEHRGqQvHUl0XnoHR+3Pb1hytAHISgSxv9h0gUiSiYtISXUU3rMrKiKzFYDrI6ZIHA== + dependencies: + "@types/chai" "^4.3.4" + "@types/chai-subset" "^1.3.3" + "@types/node" "*" + "@vitest/expect" "0.28.5" + "@vitest/runner" "0.28.5" + "@vitest/spy" "0.28.5" + "@vitest/utils" "0.28.5" + acorn "^8.8.1" + acorn-walk "^8.2.0" + cac "^6.7.14" + chai "^4.3.7" + debug "^4.3.4" + local-pkg "^0.4.2" + pathe "^1.1.0" + picocolors "^1.0.0" + source-map "^0.6.1" + std-env "^3.3.1" + strip-literal "^1.0.0" + tinybench "^2.3.1" + tinypool "^0.3.1" + tinyspy "^1.0.2" + vite "^3.0.0 || ^4.0.0" + vite-node "0.28.5" + why-is-node-running "^2.2.2" + vscode-jsonrpc@6.0.0: version "6.0.0" resolved "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-6.0.0.tgz" @@ -6203,6 +6620,14 @@ which@^2.0.1: dependencies: isexe "^2.0.0" +why-is-node-running@^2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/why-is-node-running/-/why-is-node-running-2.2.2.tgz#4185b2b4699117819e7154594271e7e344c9973e" + integrity sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA== + dependencies: + siginfo "^2.0.0" + stackback "0.0.2" + wide-align@^1.1.2: version "1.1.5" resolved "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz" @@ -6306,6 +6731,11 @@ yocto-queue@^0.1.0: resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== +yocto-queue@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.0.0.tgz#7f816433fb2cbc511ec8bf7d263c3b58a1a3c251" + integrity sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g== + zhead@^2.0.4: version "2.0.4" resolved "https://registry.npmjs.org/zhead/-/zhead-2.0.4.tgz" From eb769d789e05328906aa636624f14cf63fcab1d9 Mon Sep 17 00:00:00 2001 From: Baroshem Date: Tue, 21 Mar 2023 11:51:38 +0100 Subject: [PATCH 3/5] fix: header in playground --- playground/nuxt.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playground/nuxt.config.ts b/playground/nuxt.config.ts index 5d2b9a92..300d4dfb 100644 --- a/playground/nuxt.config.ts +++ b/playground/nuxt.config.ts @@ -14,7 +14,7 @@ export default defineNuxtConfig({ } }, headers: { - xXSSProtection: '1' + 'X-XSS-Protection': '1' }, }, }, From 6085a1ee626b8395f0ae80808782958479e58909 Mon Sep 17 00:00:00 2001 From: Baroshem Date: Tue, 21 Mar 2023 13:23:10 +0100 Subject: [PATCH 4/5] feat: add more tests and docs --- README.md | 4 +-- docs/content/1.getting-started/1.setup.md | 26 ++++++--------- .../1.getting-started/2.configuration.md | 20 ++++++++++-- docs/content/1.index.md | 4 +-- package.json | 2 +- playground/nuxt.config.ts | 5 +-- .../middleware/allowedMethodsRestricter.ts | 18 ++++++----- src/runtime/server/middleware/rateLimiter.ts | 32 ++++++++++--------- .../server/middleware/requestSizeLimiter.ts | 32 ++++++++++--------- src/runtime/server/middleware/xssValidator.ts | 30 +++++++++-------- src/types.ts | 10 +++--- test/allowedMethods.test.ts | 9 +++++- test/basicAuth.test.ts | 1 - test/fixtures/allowedMethods/app.vue | 7 ++-- test/fixtures/allowedMethods/nuxt.config.ts | 7 ++++ test/fixtures/allowedMethods/pages/index.vue | 3 ++ test/fixtures/allowedMethods/pages/test.vue | 3 ++ test/fixtures/basic/app.vue | 7 ++-- test/fixtures/basic/nuxt.config.ts | 9 +++++- test/fixtures/basic/pages/index.vue | 3 ++ test/fixtures/basic/pages/test.vue | 3 ++ test/fixtures/rateLimiter/app.vue | 7 ++-- test/fixtures/rateLimiter/nuxt.config.ts | 15 +++++++-- test/fixtures/rateLimiter/pages/index.vue | 3 ++ test/fixtures/rateLimiter/pages/test.vue | 3 ++ test/fixtures/xss/app.vue | 7 ++-- test/fixtures/xss/nuxt.config.ts | 7 ++++ test/fixtures/xss/pages/index.vue | 3 ++ test/fixtures/xss/pages/test.vue | 3 ++ test/headers.test.ts | 10 ++++++ test/rateLimiter.test.ts | 13 ++++++++ test/xssValidator.test.ts | 7 ++++ 32 files changed, 203 insertions(+), 110 deletions(-) create mode 100644 test/fixtures/allowedMethods/pages/index.vue create mode 100644 test/fixtures/allowedMethods/pages/test.vue create mode 100644 test/fixtures/basic/pages/index.vue create mode 100644 test/fixtures/basic/pages/test.vue create mode 100644 test/fixtures/rateLimiter/pages/index.vue create mode 100644 test/fixtures/rateLimiter/pages/test.vue create mode 100644 test/fixtures/xss/pages/index.vue create mode 100644 test/fixtures/xss/pages/test.vue diff --git a/README.md b/README.md index cd5b557c..d0ffeb47 100644 --- a/README.md +++ b/README.md @@ -22,9 +22,7 @@ - Request Size & Rate Limiters - Cross Site Scripting (XSS) Validation - Cross-Origin Resource Sharing (CORS) support -- Allowed HTTP Methods Restricter -- `[Optional]` Basic Auth support -- `[Optional]` CSRF support +- `[Optional]` Allowed HTTP Methods, Basic Auth, CSRF ## Usage diff --git a/docs/content/1.getting-started/1.setup.md b/docs/content/1.getting-started/1.setup.md index 63af4ea5..cd4d915d 100644 --- a/docs/content/1.getting-started/1.setup.md +++ b/docs/content/1.getting-started/1.setup.md @@ -35,28 +35,20 @@ export default defineNuxtConfig({ That's it! The Nuxt Security module will now register routeRoules and middlewares to make your application more secure ✨ :: -## Static site generation (SSG) - -This module is meant to work with SSR apps but you can also use this module in SSG apps where you will get a Content Security Policy (CSP) support. +- Security response headers +- Content Security Policy (CSP) for SSG apps +- Request Size & Rate Limiters +- Cross Site Scripting (XSS) Validation +- Cross-Origin Resource Sharing (CORS) support ::alert{type="info"} -You can find more about configuring Content Security Policy (CSP) [here](/security/headers#content-security-policy). +You can find more about configuring `nuxt-security` [here](/getting-started/configuration). :: -## Configuration - -You can add global configuration to the module like following: - -```js{}[nuxt.config.ts] -export default defineNuxtConfig({ - security: { - // options - } -}) -``` +## Static site generation (SSG) -Or per route configuration described [here](/getting-started/configuration#per-route-middleware-configuration) +This module is meant to work with SSR apps but you can also use this module in SSG apps where you will get a Content Security Policy (CSP) support. ::alert{type="info"} -You can find more about configuring `nuxt-security` [here](/getting-started/configuration). +You can find more about configuring Content Security Policy (CSP) [here](/security/headers#content-security-policy). :: diff --git a/docs/content/1.getting-started/2.configuration.md b/docs/content/1.getting-started/2.configuration.md index 05e4eb47..76d3f7e6 100644 --- a/docs/content/1.getting-started/2.configuration.md +++ b/docs/content/1.getting-started/2.configuration.md @@ -18,6 +18,16 @@ export default defineNuxtConfig({ }) ``` +You can disable them from the module configuration like following: + +```js{}[nuxt.config.ts] +export default defineNuxtConfig({ + security: { + rateLimiter: false + } +}) +``` + In general, the `security` object in nuxt configuration should be used to register functionality that will be used **globally** in your application. For per route configuration, check out the next section. ## Per route middleware configuration @@ -41,12 +51,16 @@ export default defineNuxtConfig({ By adding this you will have global middleware for all routes (regarding rate limiting) and specific configuration to the `/custom-route` route. -To avoid conflicts with global middlewares, you can disable them from the module configuration like following: +You can also disable certain middlewares per route like following: ```js{}[nuxt.config.ts] export default defineNuxtConfig({ - security: { - rateLimiter: false + routeRules: { + '/custom-route': { + security: { + rateLimiter: false + } + } } }) ``` diff --git a/docs/content/1.index.md b/docs/content/1.index.md index 1a714612..2db3c184 100644 --- a/docs/content/1.index.md +++ b/docs/content/1.index.md @@ -30,8 +30,6 @@ Security Module for Nuxt based on OWASP Top 10 and Helmet - Request Size & Rate Limiters - Cross Site Scripting (XSS) Validation - Cross-Origin Resource Sharing (CORS) support - - Allowed HTTP Methods Restricter - - `[Optional]` Basic Auth support - - `[Optional]` CSRF support + - `[Optional]` Allowed HTTP Methods, Basic Auth, CSRF :: :: diff --git a/package.json b/package.json index 9b151d10..c20da761 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "dev:generate": "nuxi generate playground", "dev:prepare": "nuxt-module-build --stub && nuxi prepare playground", "lint": "eslint --ext .js,.ts,.vue", - "test": "vitest run", + "test": "vitest run --silent", "test:watch": "vitest watch", "docs": "cd docs && yarn dev", "preview": "nuxi preview playground", diff --git a/playground/nuxt.config.ts b/playground/nuxt.config.ts index 300d4dfb..f183b3f2 100644 --- a/playground/nuxt.config.ts +++ b/playground/nuxt.config.ts @@ -8,10 +8,7 @@ export default defineNuxtConfig({ routeRules: { 'secret': { security: { - rateLimiter: { - tokensPerInterval: 2, - interval: 'hour', - } + rateLimiter: false }, headers: { 'X-XSS-Protection': '1' diff --git a/src/runtime/server/middleware/allowedMethodsRestricter.ts b/src/runtime/server/middleware/allowedMethodsRestricter.ts index 4db00b56..95eb962c 100644 --- a/src/runtime/server/middleware/allowedMethodsRestricter.ts +++ b/src/runtime/server/middleware/allowedMethodsRestricter.ts @@ -4,15 +4,17 @@ import { getRouteRules } from "#imports"; export default defineEventHandler((event) => { const routeRules = getRouteRules(event); const allowedMethods: string[] = routeRules.security.allowedMethodsRestricter; - if (!Object.values(allowedMethods).includes(event.node.req.method!!)) { - const methodNotAllowedError = { - statusCode: 405, - statusMessage: "Method not allowed", - }; + if (routeRules.security.allowedMethodsRestricter !== false) { + if (!Object.values(allowedMethods).includes(event.node.req.method!!)) { + const methodNotAllowedError = { + statusCode: 405, + statusMessage: "Method not allowed", + }; - if (routeRules.security.allowedMethodsRestricter.throwError === false) { - return methodNotAllowedError; + if (routeRules.security.allowedMethodsRestricter.throwError === false) { + return methodNotAllowedError; + } + throw createError(methodNotAllowedError); } - throw createError(methodNotAllowedError); } }); diff --git a/src/runtime/server/middleware/rateLimiter.ts b/src/runtime/server/middleware/rateLimiter.ts index 0604160d..8a92b56f 100644 --- a/src/runtime/server/middleware/rateLimiter.ts +++ b/src/runtime/server/middleware/rateLimiter.ts @@ -8,25 +8,27 @@ export default defineEventHandler(async (event) => { const routeRules = getRouteRules(event); - if (!cache.get(ip)) { - const cachedLimiter = new RateLimiter(routeRules.security.rateLimiter); - cache.put(ip, cachedLimiter, 10000); - } else { - const cachedLimiter = cache.get(ip) as RateLimiter; - - if (cachedLimiter.getTokensRemaining() > 1) { - await cachedLimiter.removeTokens(1); + if (routeRules.security.rateLimiter !== false) { + if (!cache.get(ip)) { + const cachedLimiter = new RateLimiter(routeRules.security.rateLimiter); cache.put(ip, cachedLimiter, 10000); } else { - const tooManyRequestsError = { - statusCode: 429, - statusMessage: "Too Many Requests", - }; + const cachedLimiter = cache.get(ip) as RateLimiter; + + if (cachedLimiter.getTokensRemaining() > 1) { + await cachedLimiter.removeTokens(1); + cache.put(ip, cachedLimiter, 10000); + } else { + const tooManyRequestsError = { + statusCode: 429, + statusMessage: "Too Many Requests", + }; - if (routeRules.security.rateLimiter.throwError === false) { - return tooManyRequestsError; + if (routeRules.security.rateLimiter.throwError === false) { + return tooManyRequestsError; + } + throw createError(tooManyRequestsError); } - throw createError(tooManyRequestsError); } } }); diff --git a/src/runtime/server/middleware/requestSizeLimiter.ts b/src/runtime/server/middleware/requestSizeLimiter.ts index a24a62af..498c6857 100644 --- a/src/runtime/server/middleware/requestSizeLimiter.ts +++ b/src/runtime/server/middleware/requestSizeLimiter.ts @@ -5,25 +5,27 @@ const FILE_UPLOAD_HEADER = "multipart/form-data"; export default defineEventHandler(async (event) => { const routeRules = getRouteRules(event); - if (["POST", "PUT", "DELETE"].includes(event.node.req.method!!)) { - const contentLengthValue = getRequestHeader(event, "content-length"); - const contentTypeValue = getRequestHeader(event, "content-type"); + if (routeRules.security.requestSizeLimiter !== false) { + if (["POST", "PUT", "DELETE"].includes(event.node.req.method!!)) { + const contentLengthValue = getRequestHeader(event, "content-length"); + const contentTypeValue = getRequestHeader(event, "content-type"); - const isFileUpload = contentTypeValue?.includes(FILE_UPLOAD_HEADER); + const isFileUpload = contentTypeValue?.includes(FILE_UPLOAD_HEADER); - const requestLimit = isFileUpload - ? routeRules.security.requestSizeLimiter.maxUploadFileRequestInBytes - : routeRules.security.requestSizeLimiter.maxRequestSizeInBytes; + const requestLimit = isFileUpload + ? routeRules.security.requestSizeLimiter.maxUploadFileRequestInBytes + : routeRules.security.requestSizeLimiter.maxRequestSizeInBytes; - if (parseInt(contentLengthValue as string) >= requestLimit) { - const payloadTooLargeError = { - statusCode: 413, - statusMessage: "Payload Too Large", - }; - if (routeRules.security.requestSizeLimiter.throwError === false) { - return payloadTooLargeError; + if (parseInt(contentLengthValue as string) >= requestLimit) { + const payloadTooLargeError = { + statusCode: 413, + statusMessage: "Payload Too Large", + }; + if (routeRules.security.requestSizeLimiter.throwError === false) { + return payloadTooLargeError; + } + throw createError(payloadTooLargeError); } - throw createError(payloadTooLargeError); } } }); diff --git a/src/runtime/server/middleware/xssValidator.ts b/src/runtime/server/middleware/xssValidator.ts index 45a25fcb..709d4fd6 100644 --- a/src/runtime/server/middleware/xssValidator.ts +++ b/src/runtime/server/middleware/xssValidator.ts @@ -6,21 +6,23 @@ export default defineEventHandler(async (event) => { const routeRules = getRouteRules(event); const xssValidator = new FilterXSS(routeRules.security.xssValidator); - if (["POST", "GET"].includes(event.node.req.method!!)) { - const valueToFilter = - event.node.req.method === "GET" ? getQuery(event) : await readBody(event); - // Fix for problems when one middleware is returning an error and it is catched in the next - if (valueToFilter && Object.keys(valueToFilter).length) { - if (valueToFilter.statusMessage && valueToFilter.statusMessage !== 'Bad Request') return - const stringifiedValue = JSON.stringify(valueToFilter); - const processedValue = xssValidator.process(JSON.stringify(valueToFilter)); - if (processedValue !== stringifiedValue) { - const badRequestError = { statusCode: 400, statusMessage: "Bad Request" }; - if (routeRules.security.requestSizeLimiter.throwError === false) { - return badRequestError; - } + if (routeRules.security.xssValidator !== false) { + if (["POST", "GET"].includes(event.node.req.method!!)) { + const valueToFilter = + event.node.req.method === "GET" ? getQuery(event) : await readBody(event); + // Fix for problems when one middleware is returning an error and it is catched in the next + if (valueToFilter && Object.keys(valueToFilter).length) { + if (valueToFilter.statusMessage && valueToFilter.statusMessage !== 'Bad Request') return + const stringifiedValue = JSON.stringify(valueToFilter); + const processedValue = xssValidator.process(JSON.stringify(valueToFilter)); + if (processedValue !== stringifiedValue) { + const badRequestError = { statusCode: 400, statusMessage: "Bad Request" }; + if (routeRules.security.requestSizeLimiter.throwError === false) { + return badRequestError; + } - throw createError(badRequestError); + throw createError(badRequestError); + } } } } diff --git a/src/types.ts b/src/types.ts index 5aceb659..15815bb1 100644 --- a/src/types.ts +++ b/src/types.ts @@ -277,9 +277,9 @@ export interface ModuleOptions { } export interface NuxtSecurityRouteRules { - requestSizeLimiter?: RequestSizeLimiter; - rateLimiter?: RateLimiter; - xssValidator?: XssValidator; - corsHandler?: CorsOptions; - allowedMethodsRestricter: AllowedHTTPMethods; + requestSizeLimiter?: RequestSizeLimiter | false; + rateLimiter?: RateLimiter | false; + xssValidator?: XssValidator | false; + corsHandler?: CorsOptions | false; + allowedMethodsRestricter: AllowedHTTPMethods | false; } diff --git a/test/allowedMethods.test.ts b/test/allowedMethods.test.ts index 2e3bb37b..994403cc 100644 --- a/test/allowedMethods.test.ts +++ b/test/allowedMethods.test.ts @@ -7,10 +7,17 @@ describe('[nuxt-security] Allowed Methods', async () => { rootDir: fileURLToPath(new URL('./fixtures/allowedMethods', import.meta.url)), }) - it ('should return 405 Method not allowed using not allowed HTTP Method', async () => { + it ('should return 405 Method not allowed when using not allowed HTTP Method', async () => { const res = await fetch('/') expect(res.status).toBe(405) expect(res.statusText).toBe('Method not allowed') }) + + it ('should return 200 OK when using allowed HTTP Method on route', async () => { + const res = await fetch('/test') + + expect(res.status).toBe(200) + expect(res.statusText).toBe('OK') + }) }) diff --git a/test/basicAuth.test.ts b/test/basicAuth.test.ts index 91955895..8fd23b0c 100644 --- a/test/basicAuth.test.ts +++ b/test/basicAuth.test.ts @@ -9,7 +9,6 @@ describe('[nuxt-security] Basic Auth', async () => { it ('should return 401 Access denied when not passing credentials', async () => { const res = await fetch('/') - console.log(res) expect(res.status).toBe(401) expect(res.statusText).toBe('Access denied') diff --git a/test/fixtures/allowedMethods/app.vue b/test/fixtures/allowedMethods/app.vue index 29a9c81f..2b1be090 100644 --- a/test/fixtures/allowedMethods/app.vue +++ b/test/fixtures/allowedMethods/app.vue @@ -1,6 +1,5 @@ - - diff --git a/test/fixtures/allowedMethods/nuxt.config.ts b/test/fixtures/allowedMethods/nuxt.config.ts index 011fa4f9..3e0e4ccb 100644 --- a/test/fixtures/allowedMethods/nuxt.config.ts +++ b/test/fixtures/allowedMethods/nuxt.config.ts @@ -6,5 +6,12 @@ export default defineNuxtConfig({ ], security: { allowedMethodsRestricter: ['POST'] + }, + routeRules: { + '/test': { + security: { + allowedMethodsRestricter: ['GET'] + } + } } }) diff --git a/test/fixtures/allowedMethods/pages/index.vue b/test/fixtures/allowedMethods/pages/index.vue new file mode 100644 index 00000000..8371b274 --- /dev/null +++ b/test/fixtures/allowedMethods/pages/index.vue @@ -0,0 +1,3 @@ + diff --git a/test/fixtures/allowedMethods/pages/test.vue b/test/fixtures/allowedMethods/pages/test.vue new file mode 100644 index 00000000..a1292290 --- /dev/null +++ b/test/fixtures/allowedMethods/pages/test.vue @@ -0,0 +1,3 @@ + diff --git a/test/fixtures/basic/app.vue b/test/fixtures/basic/app.vue index 29a9c81f..2b1be090 100644 --- a/test/fixtures/basic/app.vue +++ b/test/fixtures/basic/app.vue @@ -1,6 +1,5 @@ - - diff --git a/test/fixtures/basic/nuxt.config.ts b/test/fixtures/basic/nuxt.config.ts index 1bc2f7cc..f17e1497 100644 --- a/test/fixtures/basic/nuxt.config.ts +++ b/test/fixtures/basic/nuxt.config.ts @@ -3,5 +3,12 @@ import MyModule from '../../../src/module' export default defineNuxtConfig({ modules: [ MyModule - ] + ], + routeRules: { + '/test': { + headers: { + 'x-xss-protection': '1', + } + } + } }) diff --git a/test/fixtures/basic/pages/index.vue b/test/fixtures/basic/pages/index.vue new file mode 100644 index 00000000..8371b274 --- /dev/null +++ b/test/fixtures/basic/pages/index.vue @@ -0,0 +1,3 @@ + diff --git a/test/fixtures/basic/pages/test.vue b/test/fixtures/basic/pages/test.vue new file mode 100644 index 00000000..a1292290 --- /dev/null +++ b/test/fixtures/basic/pages/test.vue @@ -0,0 +1,3 @@ + diff --git a/test/fixtures/rateLimiter/app.vue b/test/fixtures/rateLimiter/app.vue index 29a9c81f..2b1be090 100644 --- a/test/fixtures/rateLimiter/app.vue +++ b/test/fixtures/rateLimiter/app.vue @@ -1,6 +1,5 @@ - - diff --git a/test/fixtures/rateLimiter/nuxt.config.ts b/test/fixtures/rateLimiter/nuxt.config.ts index 34fed450..d7c14230 100644 --- a/test/fixtures/rateLimiter/nuxt.config.ts +++ b/test/fixtures/rateLimiter/nuxt.config.ts @@ -6,9 +6,18 @@ export default defineNuxtConfig({ ], security: { rateLimiter: { - tokensPerInterval: 2, - interval: 'hour', - fireImmediately: true + tokensPerInterval: 3, + interval: 'day', + } + }, + routeRules: { + 'test': { + security: { + rateLimiter: { + tokensPerInterval: 10, + interval: 'hour', + } + } } } }) diff --git a/test/fixtures/rateLimiter/pages/index.vue b/test/fixtures/rateLimiter/pages/index.vue new file mode 100644 index 00000000..8371b274 --- /dev/null +++ b/test/fixtures/rateLimiter/pages/index.vue @@ -0,0 +1,3 @@ + diff --git a/test/fixtures/rateLimiter/pages/test.vue b/test/fixtures/rateLimiter/pages/test.vue new file mode 100644 index 00000000..a1292290 --- /dev/null +++ b/test/fixtures/rateLimiter/pages/test.vue @@ -0,0 +1,3 @@ + diff --git a/test/fixtures/xss/app.vue b/test/fixtures/xss/app.vue index 29a9c81f..2b1be090 100644 --- a/test/fixtures/xss/app.vue +++ b/test/fixtures/xss/app.vue @@ -1,6 +1,5 @@ - - diff --git a/test/fixtures/xss/nuxt.config.ts b/test/fixtures/xss/nuxt.config.ts index 778b260a..43e24405 100644 --- a/test/fixtures/xss/nuxt.config.ts +++ b/test/fixtures/xss/nuxt.config.ts @@ -4,4 +4,11 @@ export default defineNuxtConfig({ modules: [ MyModule ], + routeRules: { + 'test': { + security: { + xssValidator: false + } + } + } }) diff --git a/test/fixtures/xss/pages/index.vue b/test/fixtures/xss/pages/index.vue new file mode 100644 index 00000000..8371b274 --- /dev/null +++ b/test/fixtures/xss/pages/index.vue @@ -0,0 +1,3 @@ + diff --git a/test/fixtures/xss/pages/test.vue b/test/fixtures/xss/pages/test.vue new file mode 100644 index 00000000..a1292290 --- /dev/null +++ b/test/fixtures/xss/pages/test.vue @@ -0,0 +1,3 @@ + diff --git a/test/headers.test.ts b/test/headers.test.ts index 2abe6c42..25284539 100644 --- a/test/headers.test.ts +++ b/test/headers.test.ts @@ -15,6 +15,16 @@ describe('[nuxt-security] Headers', async () => { expect(res).toBeTruthy() }) + it('has `x-xss-protection` header set with correct default value for certain route', async () => { + const { headers } = await fetch('/test') + + expect(headers.has('x-xss-protection')).toBeTruthy() + + const xxpHeaderValue = headers.get('x-xss-protection') + + expect(xxpHeaderValue).toBeTruthy() + expect(xxpHeaderValue).toBe('1') + }) it('has `content-security-policy` header set with correct default value', async () => { const { headers } = res diff --git a/test/rateLimiter.test.ts b/test/rateLimiter.test.ts index 29bdcd0e..1498bde2 100644 --- a/test/rateLimiter.test.ts +++ b/test/rateLimiter.test.ts @@ -7,6 +7,19 @@ describe('[nuxt-security] Rate Limiter', async () => { rootDir: fileURLToPath(new URL('./fixtures/rateLimiter', import.meta.url)), }) + it ('should return 200 OK after multiple requests for certain route', async () => { + const res1 = await fetch('/test') + const res2 = await fetch('/test') + const res3 = await fetch('/test') + const res4 = await fetch('/test') + const res5 = await fetch('/test') + + expect(res1).toBeDefined() + expect(res1).toBeTruthy() + expect(res5.status).toBe(200) + expect(res5.statusText).toBe('OK') + }) + it ('should return 200 OK when not reaching the limit', async () => { const res1 = await fetch('/') const res2 = await fetch('/') diff --git a/test/xssValidator.test.ts b/test/xssValidator.test.ts index 760306a6..2e89d670 100644 --- a/test/xssValidator.test.ts +++ b/test/xssValidator.test.ts @@ -13,4 +13,11 @@ describe('[nuxt-security] Cross Site Scripting', async () => { expect(res.status).toBe(400) expect(res.statusText).toBe('Bad Request') }) + + it ('should return 200 OK when passing a script in query or body for certain route', async () => { + const res = await fetch('/test?text=') + + expect(res.status).toBe(200) + expect(res.statusText).toBe('OK') + }) }) From ec61db1077c8af705c90e92204b31e31513cb9cb Mon Sep 17 00:00:00 2001 From: Baroshem Date: Tue, 21 Mar 2023 13:25:35 +0100 Subject: [PATCH 5/5] feat: setup fix --- docs/content/1.getting-started/1.setup.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/docs/content/1.getting-started/1.setup.md b/docs/content/1.getting-started/1.setup.md index cd4d915d..65eb18fe 100644 --- a/docs/content/1.getting-started/1.setup.md +++ b/docs/content/1.getting-started/1.setup.md @@ -35,12 +35,6 @@ export default defineNuxtConfig({ That's it! The Nuxt Security module will now register routeRoules and middlewares to make your application more secure ✨ :: -- Security response headers -- Content Security Policy (CSP) for SSG apps -- Request Size & Rate Limiters -- Cross Site Scripting (XSS) Validation -- Cross-Origin Resource Sharing (CORS) support - ::alert{type="info"} You can find more about configuring `nuxt-security` [here](/getting-started/configuration). ::