Skip to content

Commit 55bb8cb

Browse files
authored
Merge pull request #650 from Baroshem/chore/2.4.0
Chore/2.4.0
2 parents 9049b87 + 5174822 commit 55bb8cb

File tree

15 files changed

+116
-57
lines changed

15 files changed

+116
-57
lines changed

docs/content/2.headers/8.strictTransportSecurity.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ You can also disable this header by `strictTransportSecurity: false`.
4747
By default, Nuxt Security will set the following value for this header.
4848

4949
```http
50-
Strict-Transport-Security: max-age=15552000; includeSubDomains;
50+
Strict-Transport-Security: max-age=15552000; includeSubDomains
5151
```
5252

5353
## Available values

docs/content/3.middleware/1.rate-limiter.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ type RateLimiter = {
6262
name: string;
6363
options: Record<string, any>;
6464
};
65+
ipHeader: string;
6566
};
6667
```
6768

@@ -118,3 +119,9 @@ rateLimiter: {
118119
}
119120
}
120121
```
122+
123+
### `ipHeader`
124+
125+
- Default: `undefined`
126+
127+
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).

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "nuxt-security",
3-
"version": "2.3.0",
3+
"version": "2.4.0",
44
"license": "MIT",
55
"type": "module",
66
"engines": {

src/defaultConfig.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ export const defaultSecurityConfig = (serverlUrl: string, strict: boolean) => {
5656
name: 'lruCache'
5757
},
5858
whiteList: undefined,
59+
ipHeader: undefined,
5960
...defaultThrowErrorValue
6061
},
6162
xssValidator: {

src/runtime/server/middleware/rateLimiter.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { defineEventHandler, createError, setResponseHeader, useStorage, getRequestIP } from '#imports'
1+
import { defineEventHandler, createError, setResponseHeader, useStorage, getRequestIP, getRequestHeader } from '#imports'
22
import type { H3Event } from 'h3'
33
import { resolveSecurityRoute, resolveSecurityRules } from '../../nitro/context'
44
import type { RateLimiter } from '../../../types/middlewares'
@@ -27,7 +27,7 @@ export default defineEventHandler(async(event) => {
2727
rules.rateLimiter,
2828
defaultRateLimiter
2929
)
30-
const ip = getIP(event)
30+
const ip = getIP(event, rateLimiter.ipHeader)
3131
if(rateLimiter.whiteList && rateLimiter.whiteList.includes(ip)){
3232
return
3333
}
@@ -89,8 +89,8 @@ async function setStorageItem(rateLimiter: Required<RateLimiter>, url: string) {
8989
await storage.setItem(url, rateLimitedObject)
9090
}
9191

92-
function getIP (event: H3Event) {
93-
const ip = getRequestIP(event, { xForwardedFor: true }) || ''
92+
function getIP (event: H3Event, customIpHeader?: string) {
93+
const ip = customIpHeader ? getRequestHeader(event, customIpHeader) || '' : getRequestIP(event, { xForwardedFor: true }) || ''
9494
return ip
9595
}
9696

src/types/middlewares.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export type RateLimiter = {
2121
headers?: boolean;
2222
whiteList?: string[];
2323
throwError?: boolean;
24+
ipHeader?: string;
2425
};
2526

2627
export type XssValidator = {

src/utils/headers.ts

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -27,25 +27,25 @@ const KEYS_TO_NAMES: Record<OptionKey, HeaderName> = {
2727
const NAMES_TO_KEYS = Object.fromEntries(Object.entries(KEYS_TO_NAMES).map(([key, name]) => ([name, key]))) as Record<HeaderName, OptionKey>
2828

2929
/**
30-
*
30+
*
3131
* Converts a valid OptionKey into its corresponding standard header name
3232
*/
3333
export function getNameFromKey(key: OptionKey) {
3434
return KEYS_TO_NAMES[key]
3535
}
3636

3737
/**
38-
*
39-
* Converts a standard header name to its corresponding OptionKey name, or undefined if not found
38+
*
39+
* Converts a standard header name to its corresponding OptionKey name, or undefined if not found
4040
*/
4141
export function getKeyFromName(headerName: string) {
4242
const [, key] = Object.entries(NAMES_TO_KEYS).find(([name]) => name.toLowerCase() === headerName.toLowerCase()) || []
4343
return key
4444
}
4545

4646
/**
47-
*
48-
* Gigen a valid OptionKey, converts a header object value into its corresponding string format
47+
*
48+
* Gigen a valid OptionKey, converts a header object value into its corresponding string format
4949
*/
5050
export function headerStringFromObject(optionKey: OptionKey, optionValue: Exclude<SecurityHeaders[OptionKey], undefined>) {
5151
// False value translates into empty header
@@ -74,11 +74,10 @@ export function headerStringFromObject(optionKey: OptionKey, optionValue: Exclud
7474
} else if (optionKey === 'strictTransportSecurity') {
7575
const policies = optionValue as StrictTransportSecurityValue
7676
return [
77-
`max-age=${policies.maxAge};`,
78-
policies.includeSubdomains && 'includeSubDomains;',
79-
policies.preload && 'preload;'
80-
].filter(Boolean).join(' ')
81-
77+
`max-age=${policies.maxAge}`,
78+
policies.includeSubdomains && 'includeSubDomains',
79+
policies.preload && 'preload'
80+
].filter(Boolean).join('; ')
8281
} else if (optionKey === 'permissionsPolicy') {
8382
const policies = optionValue as PermissionsPolicyValue
8483
return Object.entries(policies)
@@ -99,7 +98,7 @@ export function headerStringFromObject(optionKey: OptionKey, optionValue: Exclud
9998
}
10099

101100
/**
102-
*
101+
*
103102
* Given a valid OptionKey, converts a header value string into its corresponding object format
104103
*/
105104
export function headerObjectFromString(optionKey: OptionKey, headerValue: string) {
@@ -112,7 +111,7 @@ export function headerObjectFromString(optionKey: OptionKey, headerValue: string
112111
const directives = headerValue.split(';').map(directive => directive.trim()).filter(directive => directive)
113112
const objectForm = {} as ContentSecurityPolicyValue
114113
for (const directive of directives) {
115-
const [type, ...sources] = directive.split(' ').map(token => token.trim()) as [keyof ContentSecurityPolicyValue, ...string[]]
114+
const [type, ...sources] = directive.split(' ').map(token => token.trim()) as [keyof ContentSecurityPolicyValue, ...string[]]
116115
if (type === 'upgrade-insecure-requests') {
117116
objectForm[type] = true
118117
} else {
@@ -163,7 +162,6 @@ function appliesToAllResources(optionKey: OptionKey) {
163162
case 'xPermittedCrossDomainPolicies':
164163
case 'xXSSProtection':
165164
return true
166-
break
167165
default:
168166
return false
169167
}
@@ -242,4 +240,4 @@ export function backwardsCompatibleSecurity(securityHeaders?: SecurityHeaders |
242240
}
243241
})
244242
return securityHeadersAsObject
245-
}
243+
}

test/defaultHeaders.test.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ describe('[nuxt-security] Default Headers', async () => {
66
await setup({
77
rootDir: fileURLToPath(new URL('./fixtures/defaultHeaders', import.meta.url)),
88
})
9-
let res: Response
9+
let res: Response
1010

1111
it ('fetches the homepage', async () => {
1212
res = await fetch('/')
@@ -114,7 +114,7 @@ describe('[nuxt-security] Default Headers', async () => {
114114
const stsHeaderValue = headers.get('strict-transport-security')
115115

116116
expect(stsHeaderValue).toBeTruthy()
117-
expect(stsHeaderValue).toBe('max-age=15552000; includeSubDomains;')
117+
expect(stsHeaderValue).toBe('max-age=15552000; includeSubDomains')
118118
})
119119

120120
it('has `x-content-type-options` header set with default value', async () => {
@@ -183,5 +183,3 @@ describe('[nuxt-security] Default Headers', async () => {
183183
expect(xxpHeaderValue).toBe('0')
184184
})
185185
})
186-
187-

test/fixtures/perRoute/nuxt.config.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ export default defineNuxtConfig({
4747
'/provided-as-standard': {
4848
headers: {
4949
'Cross-Origin-Resource-Policy': 'cross-origin',
50-
'Strict-Transport-Security': 'max-age=1; preload;',
50+
'Strict-Transport-Security': 'max-age=1; preload',
5151
'Permissions-Policy': 'fullscreen=*, camera=(self)',
5252
'Content-Security-Policy':
5353
"script-src 'self' https:; media-src 'none';",
@@ -60,7 +60,7 @@ export default defineNuxtConfig({
6060
'Cross-Origin-Resource-Policy': 'same-site',
6161
'Cross-Origin-Opener-Policy': 'cross-origin',
6262
'Cross-Origin-Embedder-Policy': 'unsafe-none',
63-
'Strict-Transport-Security': 'max-age=1; preload;',
63+
'Strict-Transport-Security': 'max-age=1; preload',
6464
'Permissions-Policy': 'fullscreen=*',
6565
foo: 'baz',
6666
foo2: 'baz2'
@@ -95,7 +95,7 @@ export default defineNuxtConfig({
9595
},
9696
'/resolve-conflict/deep/page': {
9797
headers: {
98-
'Strict-Transport-Security': 'max-age=1; preload;',
98+
'Strict-Transport-Security': 'max-age=1; preload',
9999
'X-Frame-Options': 'DENY'
100100
},
101101
security: {

test/fixtures/rateLimiter/nuxt.config.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,5 +78,14 @@ export default defineNuxtConfig({
7878
}
7979
}
8080
},
81+
'/customIpHeader': {
82+
security: {
83+
rateLimiter: {
84+
tokensPerInterval: 0,
85+
interval: 300000,
86+
ipHeader: 'X-Custom-IP'
87+
}
88+
}
89+
},
8190
}
8291
})

0 commit comments

Comments
 (0)