From d059d2b00fb64dc1e6bc277adeb6f9de1466d57a Mon Sep 17 00:00:00 2001 From: Guillaume REMBERT Date: Thu, 28 Nov 2024 14:14:19 +0100 Subject: [PATCH 1/3] Add basic rate limiter whitelist (specific IPs only) --- playground/nuxt.config.ts | 11 ++++++++++- playground/pages/rateLimit.vue | 5 +++++ src/defaultConfig.ts | 1 + src/runtime/server/middleware/rateLimiter.ts | 3 +++ src/types/middlewares.ts | 1 + 5 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 playground/pages/rateLimit.vue diff --git a/playground/nuxt.config.ts b/playground/nuxt.config.ts index f33e0dfc..9c5e8c46 100644 --- a/playground/nuxt.config.ts +++ b/playground/nuxt.config.ts @@ -42,7 +42,16 @@ export default defineNuxtConfig({ referrerPolicy: false } } - } + }, + '/rateLimit': { + security: { + rateLimiter: { + tokensPerInterval:1, + interval: 1000, + whiteList: ['127.0.0.1'], + }, + }, + }, }, // Global configuration diff --git a/playground/pages/rateLimit.vue b/playground/pages/rateLimit.vue new file mode 100644 index 00000000..9dc35023 --- /dev/null +++ b/playground/pages/rateLimit.vue @@ -0,0 +1,5 @@ + diff --git a/src/defaultConfig.ts b/src/defaultConfig.ts index aa46f987..44e67d78 100644 --- a/src/defaultConfig.ts +++ b/src/defaultConfig.ts @@ -55,6 +55,7 @@ export const defaultSecurityConfig = (serverlUrl: string, strict: boolean) => { driver: { name: 'lruCache' }, + whiteList: undefined, ...defaultThrowErrorValue }, xssValidator: { diff --git a/src/runtime/server/middleware/rateLimiter.ts b/src/runtime/server/middleware/rateLimiter.ts index d8421f14..5b17f46e 100644 --- a/src/runtime/server/middleware/rateLimiter.ts +++ b/src/runtime/server/middleware/rateLimiter.ts @@ -28,6 +28,9 @@ export default defineEventHandler(async(event) => { defaultRateLimiter ) const ip = getIP(event) + if(rateLimiter.whiteList && rateLimiter.whiteList.includes(ip)){ + return + } const url = ip + route let storageItem = await storage.getItem(url) as StorageItem diff --git a/src/types/middlewares.ts b/src/types/middlewares.ts index 7359aaa6..fcfdab38 100644 --- a/src/types/middlewares.ts +++ b/src/types/middlewares.ts @@ -19,6 +19,7 @@ export type RateLimiter = { options?: BuiltinDriverOptions[driverName] } }[BuiltinDriverName]; headers?: boolean; + whiteList?: string[]; throwError?: boolean; }; From 848cacb5d1378844cf19b9975316f65e8f024b50 Mon Sep 17 00:00:00 2001 From: Guillaume REMBERT Date: Thu, 28 Nov 2024 14:23:35 +0100 Subject: [PATCH 2/3] Update doc for basic rate limiter whitelist (specific IPs only) + fix minor orthograph --- docs/content/3.middleware/1.rate-limiter.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/content/3.middleware/1.rate-limiter.md b/docs/content/3.middleware/1.rate-limiter.md index 0a05e367..66547577 100644 --- a/docs/content/3.middleware/1.rate-limiter.md +++ b/docs/content/3.middleware/1.rate-limiter.md @@ -56,6 +56,7 @@ type RateLimiter = { tokensPerInterval: number; interval: number; headers: boolean; + whiteList: string[]; throwError: boolean; driver: { name: string; @@ -82,11 +83,17 @@ The time value in miliseconds after which the rate limiting will be reset. For e When set to `true` it will set the response headers: `X-Ratelimit-Remaining`, `X-Ratelimit-Reset`, `X-Ratelimit-Limit` with appriopriate values. +### `whiteList` + +- Default: `undefined` + +When set to `['127.0.0.1', '192.168.0.1']` it will skip rate limiting for these specific IPs. + ### `throwError` - Default: `true` -Whether to throw Nuxt Error with appriopriate error code and message. If set to false, it will just return the object with the error that you can handle. +Whether to throw Nuxt Error with appropriate error code and message. If set to false, it will just return the object with the error that you can handle. ### `driver` From 524b00378f096c0db0623e3bfff775df59570a5f Mon Sep 17 00:00:00 2001 From: Guillaume REMBERT Date: Thu, 28 Nov 2024 15:56:20 +0100 Subject: [PATCH 3/3] Add whitelist automatic tests --- test/fixtures/rateLimiter/nuxt.config.ts | 64 ++++++++++++++++++- .../rateLimiter/pages/whitelistBase.vue | 3 + .../rateLimiter/pages/whitelistEmpty.vue | 3 + .../rateLimiter/pages/whitelistNotListed.vue | 3 + test/rateLimiter.test.ts | 39 +++++++++++ 5 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 test/fixtures/rateLimiter/pages/whitelistBase.vue create mode 100644 test/fixtures/rateLimiter/pages/whitelistEmpty.vue create mode 100644 test/fixtures/rateLimiter/pages/whitelistNotListed.vue diff --git a/test/fixtures/rateLimiter/nuxt.config.ts b/test/fixtures/rateLimiter/nuxt.config.ts index 276ce3a0..de35fc0a 100644 --- a/test/fixtures/rateLimiter/nuxt.config.ts +++ b/test/fixtures/rateLimiter/nuxt.config.ts @@ -15,6 +15,68 @@ export default defineNuxtConfig({ tokensPerInterval: 10, } } - } + }, + '/whitelistBase': { + security: { + rateLimiter: { + tokensPerInterval: 1, + interval: 300000, + whiteList: [ + '127.0.0.1', + '192.168.0.1', + '172.16.0.1', + '10.0.0.1', + ], + } + } + }, + '/whitelistEmpty': { + security: { + rateLimiter: { + tokensPerInterval: 1, + interval: 300000, + whiteList: [], + } + } + }, + '/whitelistNotListed': { + security: { + rateLimiter: { + tokensPerInterval: 1, + interval: 300000, + whiteList: [ + '10.0.0.1', + '10.0.1.1', + '10.0.2.1', + '10.0.3.1', + '10.0.4.1', + '10.0.5.1', + '10.0.6.1', + '10.0.7.1', + '10.0.8.1', + '10.0.9.1', + '10.1.0.1', + '10.2.0.1', + '10.3.0.1', + '10.4.0.1', + '10.5.0.1', + '10.6.0.1', + '10.7.0.1', + '10.8.0.1', + '10.9.0.1', + '192.168.0.1', + '192.168.1.1', + '192.168.2.1', + '192.168.3.1', + '192.168.4.1', + '192.168.5.1', + '192.168.6.1', + '192.168.7.1', + '192.168.8.1', + '192.168.9.1', + ], + } + } + }, } }) diff --git a/test/fixtures/rateLimiter/pages/whitelistBase.vue b/test/fixtures/rateLimiter/pages/whitelistBase.vue new file mode 100644 index 00000000..0e6f9208 --- /dev/null +++ b/test/fixtures/rateLimiter/pages/whitelistBase.vue @@ -0,0 +1,3 @@ + diff --git a/test/fixtures/rateLimiter/pages/whitelistEmpty.vue b/test/fixtures/rateLimiter/pages/whitelistEmpty.vue new file mode 100644 index 00000000..63999ff6 --- /dev/null +++ b/test/fixtures/rateLimiter/pages/whitelistEmpty.vue @@ -0,0 +1,3 @@ + diff --git a/test/fixtures/rateLimiter/pages/whitelistNotListed.vue b/test/fixtures/rateLimiter/pages/whitelistNotListed.vue new file mode 100644 index 00000000..b00819ba --- /dev/null +++ b/test/fixtures/rateLimiter/pages/whitelistNotListed.vue @@ -0,0 +1,3 @@ + diff --git a/test/rateLimiter.test.ts b/test/rateLimiter.test.ts index fbe17974..0dda16f5 100644 --- a/test/rateLimiter.test.ts +++ b/test/rateLimiter.test.ts @@ -63,4 +63,43 @@ describe('[nuxt-security] Rate Limiter', async () => { expect(res6.status).toBe(200) expect(res6.statusText).toBe('OK') }) + + it ('should return 200 OK after multiple requests for a route with localhost ip whitelisted', async () => { + const res1 = await fetch('/whitelistBase') + await fetch('/whitelistBase') + await fetch('/whitelistBase') + await fetch('/whitelistBase') + const res5 = await fetch('/whitelistBase') + + expect(res1).toBeDefined() + expect(res1).toBeTruthy() + expect(res5.status).toBe(200) + expect(res5.statusText).toBe('OK') + }) + + it ('should return 429 when limit reached with an empty whitelist array', async () => { + const res1 = await fetch('/whitelistEmpty') + await fetch('/whitelistEmpty') + await fetch('/whitelistEmpty') + await fetch('/whitelistEmpty') + const res5 = await fetch('/whitelistEmpty') + + expect(res1).toBeDefined() + expect(res1).toBeTruthy() + expect(res5.status).toBe(429) + expect(res5.statusText).toBe('Too Many Requests') + }) + + it ('should return 429 when limit reached as localhost ip is not whitelisted', async () => { + const res1 = await fetch('/whitelistNotListed') + await fetch('/whitelistNotListed') + await fetch('/whitelistNotListed') + await fetch('/whitelistNotListed') + const res5 = await fetch('/whitelistNotListed') + + expect(res1).toBeDefined() + expect(res1).toBeTruthy() + expect(res5.status).toBe(429) + expect(res5.statusText).toBe('Too Many Requests') + }) })