diff --git a/docs/content/2.headers/8.strictTransportSecurity.md b/docs/content/2.headers/8.strictTransportSecurity.md index 3d60ef6c..964aade3 100644 --- a/docs/content/2.headers/8.strictTransportSecurity.md +++ b/docs/content/2.headers/8.strictTransportSecurity.md @@ -47,7 +47,7 @@ You can also disable this header by `strictTransportSecurity: false`. By default, Nuxt Security will set the following value for this header. ```http -Strict-Transport-Security: max-age=15552000; includeSubDomains; +Strict-Transport-Security: max-age=15552000; includeSubDomains ``` ## Available values diff --git a/docs/content/3.middleware/1.rate-limiter.md b/docs/content/3.middleware/1.rate-limiter.md index 66547577..013f8914 100644 --- a/docs/content/3.middleware/1.rate-limiter.md +++ b/docs/content/3.middleware/1.rate-limiter.md @@ -62,6 +62,7 @@ type RateLimiter = { name: string; options: Record; }; + ipHeader: string; }; ``` @@ -118,3 +119,9 @@ rateLimiter: { } } ``` + +### `ipHeader` + +- Default: `undefined` + +A custom header name (string) that will be used to determine the IP address of the request. Useful when the default `x-forwarded-for` header can not be used, and you want to use an alternative like [cf-connecting-ip](https://developers.cloudflare.com/fundamentals/reference/http-headers/#cf-connecting-ip). \ No newline at end of file diff --git a/package.json b/package.json index 4e3a2ec2..4bcf0109 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nuxt-security", - "version": "2.3.0", + "version": "2.4.0", "license": "MIT", "type": "module", "engines": { diff --git a/src/defaultConfig.ts b/src/defaultConfig.ts index 44e67d78..610de3ee 100644 --- a/src/defaultConfig.ts +++ b/src/defaultConfig.ts @@ -56,6 +56,7 @@ export const defaultSecurityConfig = (serverlUrl: string, strict: boolean) => { name: 'lruCache' }, whiteList: undefined, + ipHeader: undefined, ...defaultThrowErrorValue }, xssValidator: { diff --git a/src/runtime/server/middleware/rateLimiter.ts b/src/runtime/server/middleware/rateLimiter.ts index 5b17f46e..f5755ee9 100644 --- a/src/runtime/server/middleware/rateLimiter.ts +++ b/src/runtime/server/middleware/rateLimiter.ts @@ -1,4 +1,4 @@ -import { defineEventHandler, createError, setResponseHeader, useStorage, getRequestIP } from '#imports' +import { defineEventHandler, createError, setResponseHeader, useStorage, getRequestIP, getRequestHeader } from '#imports' import type { H3Event } from 'h3' import { resolveSecurityRoute, resolveSecurityRules } from '../../nitro/context' import type { RateLimiter } from '../../../types/middlewares' @@ -27,7 +27,7 @@ export default defineEventHandler(async(event) => { rules.rateLimiter, defaultRateLimiter ) - const ip = getIP(event) + const ip = getIP(event, rateLimiter.ipHeader) if(rateLimiter.whiteList && rateLimiter.whiteList.includes(ip)){ return } @@ -89,8 +89,8 @@ async function setStorageItem(rateLimiter: Required, url: string) { await storage.setItem(url, rateLimitedObject) } -function getIP (event: H3Event) { - const ip = getRequestIP(event, { xForwardedFor: true }) || '' +function getIP (event: H3Event, customIpHeader?: string) { + const ip = customIpHeader ? getRequestHeader(event, customIpHeader) || '' : getRequestIP(event, { xForwardedFor: true }) || '' return ip } diff --git a/src/types/middlewares.ts b/src/types/middlewares.ts index fcfdab38..f62e4e39 100644 --- a/src/types/middlewares.ts +++ b/src/types/middlewares.ts @@ -21,6 +21,7 @@ export type RateLimiter = { headers?: boolean; whiteList?: string[]; throwError?: boolean; + ipHeader?: string; }; export type XssValidator = { diff --git a/src/utils/headers.ts b/src/utils/headers.ts index 2af9c0cd..f3b0ac94 100644 --- a/src/utils/headers.ts +++ b/src/utils/headers.ts @@ -27,7 +27,7 @@ const KEYS_TO_NAMES: Record = { const NAMES_TO_KEYS = Object.fromEntries(Object.entries(KEYS_TO_NAMES).map(([key, name]) => ([name, key]))) as Record /** - * + * * Converts a valid OptionKey into its corresponding standard header name */ export function getNameFromKey(key: OptionKey) { @@ -35,8 +35,8 @@ export function getNameFromKey(key: OptionKey) { } /** - * - * Converts a standard header name to its corresponding OptionKey name, or undefined if not found + * + * Converts a standard header name to its corresponding OptionKey name, or undefined if not found */ export function getKeyFromName(headerName: string) { const [, key] = Object.entries(NAMES_TO_KEYS).find(([name]) => name.toLowerCase() === headerName.toLowerCase()) || [] @@ -44,8 +44,8 @@ export function getKeyFromName(headerName: string) { } /** - * - * Gigen a valid OptionKey, converts a header object value into its corresponding string format + * + * Gigen a valid OptionKey, converts a header object value into its corresponding string format */ export function headerStringFromObject(optionKey: OptionKey, optionValue: Exclude) { // False value translates into empty header @@ -74,11 +74,10 @@ export function headerStringFromObject(optionKey: OptionKey, optionValue: Exclud } else if (optionKey === 'strictTransportSecurity') { const policies = optionValue as StrictTransportSecurityValue return [ - `max-age=${policies.maxAge};`, - policies.includeSubdomains && 'includeSubDomains;', - policies.preload && 'preload;' - ].filter(Boolean).join(' ') - + `max-age=${policies.maxAge}`, + policies.includeSubdomains && 'includeSubDomains', + policies.preload && 'preload' + ].filter(Boolean).join('; ') } else if (optionKey === 'permissionsPolicy') { const policies = optionValue as PermissionsPolicyValue return Object.entries(policies) @@ -99,7 +98,7 @@ export function headerStringFromObject(optionKey: OptionKey, optionValue: Exclud } /** - * + * * Given a valid OptionKey, converts a header value string into its corresponding object format */ export function headerObjectFromString(optionKey: OptionKey, headerValue: string) { @@ -112,7 +111,7 @@ export function headerObjectFromString(optionKey: OptionKey, headerValue: string const directives = headerValue.split(';').map(directive => directive.trim()).filter(directive => directive) const objectForm = {} as ContentSecurityPolicyValue for (const directive of directives) { - const [type, ...sources] = directive.split(' ').map(token => token.trim()) as [keyof ContentSecurityPolicyValue, ...string[]] + const [type, ...sources] = directive.split(' ').map(token => token.trim()) as [keyof ContentSecurityPolicyValue, ...string[]] if (type === 'upgrade-insecure-requests') { objectForm[type] = true } else { @@ -163,7 +162,6 @@ function appliesToAllResources(optionKey: OptionKey) { case 'xPermittedCrossDomainPolicies': case 'xXSSProtection': return true - break default: return false } @@ -242,4 +240,4 @@ export function backwardsCompatibleSecurity(securityHeaders?: SecurityHeaders | } }) return securityHeadersAsObject -} \ No newline at end of file +} diff --git a/test/defaultHeaders.test.ts b/test/defaultHeaders.test.ts index 5fc307ae..60a7e284 100644 --- a/test/defaultHeaders.test.ts +++ b/test/defaultHeaders.test.ts @@ -6,7 +6,7 @@ describe('[nuxt-security] Default Headers', async () => { await setup({ rootDir: fileURLToPath(new URL('./fixtures/defaultHeaders', import.meta.url)), }) - let res: Response + let res: Response it ('fetches the homepage', async () => { res = await fetch('/') @@ -114,7 +114,7 @@ describe('[nuxt-security] Default Headers', async () => { const stsHeaderValue = headers.get('strict-transport-security') expect(stsHeaderValue).toBeTruthy() - expect(stsHeaderValue).toBe('max-age=15552000; includeSubDomains;') + expect(stsHeaderValue).toBe('max-age=15552000; includeSubDomains') }) it('has `x-content-type-options` header set with default value', async () => { @@ -183,5 +183,3 @@ describe('[nuxt-security] Default Headers', async () => { expect(xxpHeaderValue).toBe('0') }) }) - - diff --git a/test/fixtures/perRoute/nuxt.config.ts b/test/fixtures/perRoute/nuxt.config.ts index ae0fc770..6b61c4c0 100644 --- a/test/fixtures/perRoute/nuxt.config.ts +++ b/test/fixtures/perRoute/nuxt.config.ts @@ -47,7 +47,7 @@ export default defineNuxtConfig({ '/provided-as-standard': { headers: { 'Cross-Origin-Resource-Policy': 'cross-origin', - 'Strict-Transport-Security': 'max-age=1; preload;', + 'Strict-Transport-Security': 'max-age=1; preload', 'Permissions-Policy': 'fullscreen=*, camera=(self)', 'Content-Security-Policy': "script-src 'self' https:; media-src 'none';", @@ -60,7 +60,7 @@ export default defineNuxtConfig({ 'Cross-Origin-Resource-Policy': 'same-site', 'Cross-Origin-Opener-Policy': 'cross-origin', 'Cross-Origin-Embedder-Policy': 'unsafe-none', - 'Strict-Transport-Security': 'max-age=1; preload;', + 'Strict-Transport-Security': 'max-age=1; preload', 'Permissions-Policy': 'fullscreen=*', foo: 'baz', foo2: 'baz2' @@ -95,7 +95,7 @@ export default defineNuxtConfig({ }, '/resolve-conflict/deep/page': { headers: { - 'Strict-Transport-Security': 'max-age=1; preload;', + 'Strict-Transport-Security': 'max-age=1; preload', 'X-Frame-Options': 'DENY' }, security: { diff --git a/test/fixtures/rateLimiter/nuxt.config.ts b/test/fixtures/rateLimiter/nuxt.config.ts index de35fc0a..da7dd8bb 100644 --- a/test/fixtures/rateLimiter/nuxt.config.ts +++ b/test/fixtures/rateLimiter/nuxt.config.ts @@ -78,5 +78,14 @@ export default defineNuxtConfig({ } } }, + '/customIpHeader': { + security: { + rateLimiter: { + tokensPerInterval: 0, + interval: 300000, + ipHeader: 'X-Custom-IP' + } + } + }, } }) diff --git a/test/fixtures/rateLimiter/pages/customIpHeader.vue b/test/fixtures/rateLimiter/pages/customIpHeader.vue new file mode 100644 index 00000000..08c4c209 --- /dev/null +++ b/test/fixtures/rateLimiter/pages/customIpHeader.vue @@ -0,0 +1,3 @@ + diff --git a/test/perRoute.test.ts b/test/perRoute.test.ts index bc4570f8..faf80633 100644 --- a/test/perRoute.test.ts +++ b/test/perRoute.test.ts @@ -32,7 +32,7 @@ describe('[nuxt-security] Per-route Configuration', async () => { expect(csp).toBe("base-uri 'none'; font-src 'self' https: data:; form-action 'self'; frame-ancestors 'self'; img-src 'self' data:; object-src 'none'; script-src-attr 'none'; style-src 'self' https: 'unsafe-inline'; script-src 'self' https: 'unsafe-inline' 'strict-dynamic'; upgrade-insecure-requests;") expect(oac).toBe('?1') expect(rp).toBe('no-referrer') - expect(sts).toBe('max-age=15552000; includeSubDomains;') + expect(sts).toBe('max-age=15552000; includeSubDomains') expect(xcto).toBe('nosniff') expect(xdpc).toBe('off') expect(xdo).toBe('noopen') @@ -70,7 +70,7 @@ describe('[nuxt-security] Per-route Configuration', async () => { expect(csp).toBe("base-uri 'none'; font-src 'self' https: data:; form-action 'self'; frame-ancestors 'self'; img-src 'self' data:; object-src 'none'; script-src-attr 'none'; style-src 'self' https: 'unsafe-inline'; script-src 'self' https: 'unsafe-inline' 'strict-dynamic'; upgrade-insecure-requests;") expect(oac).toBe('?1') expect(rp).toBe('no-referrer') - expect(sts).toBe('max-age=15552000; includeSubDomains;') + expect(sts).toBe('max-age=15552000; includeSubDomains') expect(xcto).toBe('nosniff') expect(xdpc).toBe('off') expect(xdo).toBe('noopen') @@ -108,7 +108,7 @@ describe('[nuxt-security] Per-route Configuration', async () => { expect(csp).toBe("base-uri 'none'; font-src 'self' https: data:; form-action 'self'; frame-ancestors 'self'; img-src 'self' data:; object-src 'none'; script-src-attr 'none'; style-src 'self' https: 'unsafe-inline'; script-src 'self' https: 'unsafe-inline' 'strict-dynamic'; upgrade-insecure-requests;") expect(oac).toBe('?1') expect(rp).toBe('no-referrer') - expect(sts).toBe('max-age=15552000; includeSubDomains;') + expect(sts).toBe('max-age=15552000; includeSubDomains') expect(xcto).toBe('nosniff') expect(xdpc).toBe('off') expect(xdo).toBe('noopen') @@ -152,7 +152,7 @@ describe('[nuxt-security] Per-route Configuration', async () => { const xxp = headers.get('x-xss-protection') expect(rp).toBe('no-referrer') - expect(sts).toBe('max-age=15552000; includeSubDomains;') + expect(sts).toBe('max-age=15552000; includeSubDomains') expect(xcto).toBe('nosniff') expect(xdo).toBe('noopen') expect(xfo).toBe('SAMEORIGIN') @@ -194,7 +194,7 @@ describe('[nuxt-security] Per-route Configuration', async () => { const xxp = headers.get('x-xss-protection') expect(rp).toBe('no-referrer') - expect(sts).toBe('max-age=15552000; includeSubDomains;') + expect(sts).toBe('max-age=15552000; includeSubDomains') expect(xcto).toBe('nosniff') expect(xdo).toBe('noopen') expect(xfo).toBe('SAMEORIGIN') @@ -230,7 +230,7 @@ describe('[nuxt-security] Per-route Configuration', async () => { expect(csp).toBe("base-uri 'none'; font-src 'self' https: data:; form-action 'self'; frame-ancestors 'self'; img-src 'self' data:; object-src 'none'; script-src-attr 'none'; style-src 'self' https: 'unsafe-inline'; script-src 'self' https: 'unsafe-inline' 'strict-dynamic'; upgrade-insecure-requests;") expect(oac).toBe('?1') expect(rp).toBe('no-referrer') - expect(sts).toBe('max-age=15552000; includeSubDomains;') + expect(sts).toBe('max-age=15552000; includeSubDomains') expect(xcto).toBe('nosniff') expect(xdpc).toBe('off') expect(xdo).toBe('noopen') @@ -268,7 +268,7 @@ describe('[nuxt-security] Per-route Configuration', async () => { expect(csp).toBe("base-uri 'none'; font-src 'self' https: data:; form-action 'self'; frame-ancestors 'self'; img-src 'self' data:; object-src 'none'; script-src-attr 'none'; style-src 'self' https: 'unsafe-inline'; script-src 'self' https: 'unsafe-inline' 'strict-dynamic'; upgrade-insecure-requests;") expect(oac).toBe('?1') expect(rp).toBe('no-referrer') - expect(sts).toBe('max-age=15552000; includeSubDomains;') + expect(sts).toBe('max-age=15552000; includeSubDomains') expect(xcto).toBe('nosniff') expect(xdpc).toBe('off') expect(xdo).toBe('noopen') @@ -306,7 +306,7 @@ describe('[nuxt-security] Per-route Configuration', async () => { expect(csp).toBe("base-uri 'none'; font-src 'self' https: data:; form-action 'self'; frame-ancestors 'self'; img-src 'self' data:; object-src 'none'; script-src-attr 'none'; style-src 'self' https: 'unsafe-inline'; script-src 'self' https: 'unsafe-inline' 'strict-dynamic'; upgrade-insecure-requests;") expect(oac).toBe('?1') expect(rp).toBeNull() - expect(sts).toBe('max-age=15552000; includeSubDomains;') + expect(sts).toBe('max-age=15552000; includeSubDomains') expect(xcto).toBe('nosniff') expect(xdpc).toBe('off') expect(xdo).toBe('noopen') @@ -344,7 +344,7 @@ describe('[nuxt-security] Per-route Configuration', async () => { expect(csp).toBe("base-uri 'none'; font-src 'self' https: data:; form-action 'self'; frame-ancestors 'self'; img-src 'self' data:; object-src 'none'; script-src-attr 'none'; style-src 'self' https: 'unsafe-inline'; script-src 'self' https: 'unsafe-inline' 'strict-dynamic'; upgrade-insecure-requests;") expect(oac).toBe('?1') expect(rp).toBeNull() - expect(sts).toBe('max-age=15552000; includeSubDomains;') + expect(sts).toBe('max-age=15552000; includeSubDomains') expect(xcto).toBe('nosniff') expect(xdpc).toBe('off') expect(xdo).toBe('noopen') @@ -382,7 +382,7 @@ describe('[nuxt-security] Per-route Configuration', async () => { expect(csp).toBe("base-uri 'none'; font-src 'self' https: data:; form-action 'self'; frame-ancestors 'self'; img-src 'self' data:; object-src 'none'; script-src-attr 'none'; style-src 'self' https: 'unsafe-inline'; script-src 'self' https: 'unsafe-inline' 'strict-dynamic'; upgrade-insecure-requests;") expect(oac).toBe('?1') expect(rp).toBe('no-referrer') - expect(sts).toBe('max-age=15552000; includeSubDomains;') + expect(sts).toBe('max-age=15552000; includeSubDomains') expect(xcto).toBe('nosniff') expect(xdpc).toBe('off') expect(xdo).toBe('noopen') @@ -420,7 +420,7 @@ describe('[nuxt-security] Per-route Configuration', async () => { expect(csp).toBe("base-uri 'none'; font-src 'self' https: data:; form-action 'self'; frame-ancestors 'self'; img-src 'self' data:; object-src 'none'; script-src-attr 'none'; style-src 'self' https: 'unsafe-inline'; script-src 'self' https: 'unsafe-inline' 'strict-dynamic'; upgrade-insecure-requests;") expect(oac).toBe('?1') expect(rp).toBe('no-referrer') - expect(sts).toBe('max-age=15552000; includeSubDomains;') + expect(sts).toBe('max-age=15552000; includeSubDomains') expect(xcto).toBe('nosniff') expect(xdpc).toBe('off') expect(xdo).toBe('noopen') @@ -458,7 +458,7 @@ describe('[nuxt-security] Per-route Configuration', async () => { expect(csp).toBe("base-uri 'none'; font-src 'self' https: data:; form-action 'self'; frame-ancestors 'self'; img-src 'self' data:; object-src 'none'; script-src-attr 'none'; style-src 'self' https: 'unsafe-inline'; script-src 'self' https:; upgrade-insecure-requests; media-src 'none';") expect(oac).toBe('?1') expect(rp).toBe('no-referrer') - expect(sts).toBe('max-age=1; includeSubDomains; preload;') + expect(sts).toBe('max-age=1; includeSubDomains; preload') expect(xcto).toBe('nosniff') expect(xdpc).toBe('off') expect(xdo).toBe('noopen') @@ -498,7 +498,7 @@ describe('[nuxt-security] Per-route Configuration', async () => { expect(csp).toBe("base-uri 'none'; font-src 'self' https: data:; form-action 'self'; frame-ancestors 'self'; img-src 'self' data:; object-src 'none'; script-src-attr 'none'; style-src 'self' https: 'unsafe-inline'; script-src 'self' https: 'unsafe-inline' 'strict-dynamic'; upgrade-insecure-requests;") expect(oac).toBe('?1') expect(rp).toBe('no-referrer') - expect(sts).toBe('max-age=2;') + expect(sts).toBe('max-age=2') expect(xcto).toBe('nosniff') expect(xdpc).toBe('off') expect(xdo).toBe('noopen') @@ -538,7 +538,7 @@ describe('[nuxt-security] Per-route Configuration', async () => { expect(csp).toBe("base-uri 'none'; font-src 'self' https: data:; form-action 'self'; frame-ancestors 'self'; img-src 'self' data:; object-src 'none'; script-src-attr 'none'; style-src 'self' https: 'unsafe-inline'; script-src 'self' https: 'unsafe-inline' 'strict-dynamic'; upgrade-insecure-requests;") expect(oac).toBe('?1') expect(rp).toBe('no-referrer-when-downgrade') - expect(sts).toBe('max-age=1; preload;') + expect(sts).toBe('max-age=1; preload') expect(xcto).toBe('nosniff') expect(xdpc).toBe('off') expect(xdo).toBe('noopen') @@ -579,7 +579,7 @@ describe('[nuxt-security] Per-route Configuration', async () => { expect(csp).toBe("font-src 'self' https: data:; form-action 'self'; frame-ancestors 'self'; img-src https:; object-src 'none'; script-src-attr 'none'; style-src 'self' https: 'unsafe-inline'; script-src 'self'; upgrade-insecure-requests;") expect(oac).toBe('?1') expect(rp).toBe('no-referrer') - expect(sts).toBe('max-age=10; preload;') + expect(sts).toBe('max-age=10; preload') expect(xcto).toBe('nosniff') expect(xdpc).toBe('off') expect(xdo).toBe('noopen') @@ -617,7 +617,7 @@ describe('[nuxt-security] Per-route Configuration', async () => { expect(csp).toBe("base-uri 'none'; font-src 'self' https: data:; form-action 'self'; frame-ancestors 'self'; img-src 'self' data:; object-src 'none'; script-src-attr 'none'; style-src 'self' https: 'unsafe-inline'; script-src 'self'; upgrade-insecure-requests; manifest-src 'none';") expect(oac).toBe('?1') expect(rp).toBe('no-referrer') - expect(sts).toBe('max-age=10; includeSubDomains;') + expect(sts).toBe('max-age=10; includeSubDomains') expect(xcto).toBe('nosniff') expect(xdpc).toBe('off') expect(xdo).toBe('noopen') @@ -697,13 +697,13 @@ describe('[nuxt-security] Per-route Configuration', async () => { const xxp = headers.get('x-xss-protection') expect(rp).toBe('no-referrer') - expect(sts).toBe('max-age=15552000; includeSubDomains;') + expect(sts).toBe('max-age=15552000; includeSubDomains') expect(xcto).toBe('nosniff') expect(xdo).toBe('noopen') expect(xfo).toBe('SAMEORIGIN') expect(xpcdp).toBe('none') expect(xxp).toBe('0') - + }) it('sets all-resources security headers for bundled assets', async () => { @@ -746,7 +746,7 @@ describe('[nuxt-security] Per-route Configuration', async () => { const xxp = headers.get('x-xss-protection') expect(rp).toBe('no-referrer') - expect(sts).toBe('max-age=15552000; includeSubDomains;') + expect(sts).toBe('max-age=15552000; includeSubDomains') expect(xcto).toBe('nosniff') expect(xdo).toBe('noopen') expect(xfo).toBe('SAMEORIGIN') @@ -785,7 +785,7 @@ describe('[nuxt-security] Per-route Configuration', async () => { const xxp = headers.get('x-xss-protection') expect(rp).toBe('no-referrer') - expect(sts).toBe('max-age=15552000; includeSubDomains;') + expect(sts).toBe('max-age=15552000; includeSubDomains') expect(xcto).toBe('nosniff') expect(xdo).toBe('noopen') expect(xfo).toBe('SAMEORIGIN') @@ -977,5 +977,3 @@ describe('[nuxt-security] Per-route Configuration', async () => { expect(rp).toBeNull() }) }) - - diff --git a/test/publicAssets.test.ts b/test/publicAssets.test.ts index c10a9b25..02413f6e 100644 --- a/test/publicAssets.test.ts +++ b/test/publicAssets.test.ts @@ -10,7 +10,7 @@ describe('[nuxt-security] Public Assets', async () => { it('does not set all-resources security headers when disabled in config', async () => { const { headers } = await fetch('/icon.png') expect(headers).toBeDefined() - + // Security headers that are always set on all resources const rp = headers.get('referrer-policy') const sts = headers.get('strict-transport-security') @@ -28,11 +28,11 @@ describe('[nuxt-security] Public Assets', async () => { expect(xpcdp).toBeNull() expect(xxp).toBeNull() }) - + it('sets security headers on routes when specified in routeRules', async () => { const { headers } = await fetch('/test/icon.png') expect(headers).toBeDefined() - + // Security headers that are always set on all resources const rp = headers.get('referrer-policy') const sts = headers.get('strict-transport-security') @@ -43,7 +43,7 @@ describe('[nuxt-security] Public Assets', async () => { const xxp = headers.get('x-xss-protection') expect(rp).toBe('no-referrer') - expect(sts).toBe('max-age=15552000; includeSubDomains;') + expect(sts).toBe('max-age=15552000; includeSubDomains') expect(xcto).toBe('nosniff') expect(xdo).toBe('noopen') expect(xfo).toBe('SAMEORIGIN') diff --git a/test/rateLimiter.test.ts b/test/rateLimiter.test.ts index 0dda16f5..548aabfb 100644 --- a/test/rateLimiter.test.ts +++ b/test/rateLimiter.test.ts @@ -102,4 +102,50 @@ describe('[nuxt-security] Rate Limiter', async () => { expect(res5.status).toBe(429) expect(res5.statusText).toBe('Too Many Requests') }) + + it ('should return 200 OK after multiple requests for a route with different IPs in the custom ipHeader', async () => { + const count = 5 + const requests = Array.from({ length: count }, (value, index) => + fetch('/customIpHeader', { + headers: { 'X-Custom-IP': `${index}` } + }).then((res) => res.status) + ) + + const results = await Promise.allSettled(requests) + + expect(results).toBeDefined() + expect(results).toBeTruthy() + expect(results.length).toEqual(count) + + for (const result of results) { + expect(result).toMatchObject({ status: 'fulfilled', value: 200 }) + } + }) + + it ('should return 429 after multiple requests for a route with the same IPs in the custom ipHeader', async () => { + const count = 5 + const firstAttempts = Array.from({ length: count }, (value, index) => + fetch('/customIpHeader', { + headers: { 'X-Custom-IP': `${index}` } + }).then((res) => res.status) + ) + + await Promise.allSettled(firstAttempts) + + const retries = Array.from({ length: count }, (value, index) => + fetch('/customIpHeader', { + headers: { 'X-Custom-IP': `${index}` } + }).then((res) => res.status) + ) + + const retryResults = await Promise.allSettled(retries) + + expect(retryResults).toBeDefined() + expect(retryResults).toBeTruthy() + expect(retryResults.length).toEqual(count) + + for (const result of retryResults) { + expect(result).toMatchObject({ status: 'fulfilled', value: 429 }) + } + }) }) diff --git a/test/strictHeaders.test.ts b/test/strictHeaders.test.ts index 205dc76b..479128cb 100644 --- a/test/strictHeaders.test.ts +++ b/test/strictHeaders.test.ts @@ -6,7 +6,7 @@ describe('[nuxt-security] Strict Headers', async () => { await setup({ rootDir: fileURLToPath(new URL('./fixtures/strictHeaders', import.meta.url)), }) - let res: Response + let res: Response it ('fetches the homepage', async () => { res = await fetch('/') @@ -114,7 +114,7 @@ describe('[nuxt-security] Strict Headers', async () => { const stsHeaderValue = headers.get('strict-transport-security') expect(stsHeaderValue).toBeTruthy() - expect(stsHeaderValue).toBe('max-age=31536000; includeSubDomains; preload;') + expect(stsHeaderValue).toBe('max-age=31536000; includeSubDomains; preload') }) it('has `x-content-type-options` header set with strict value', async () => { @@ -183,5 +183,3 @@ describe('[nuxt-security] Strict Headers', async () => { expect(xxpHeaderValue).toBe('0') }) }) - -