Skip to content

Commit 41b743a

Browse files
Netailijjk
andauthored
fix: server functions x-forwarded-host possible multiple values (vercel#73701)
## Summary The x-forwarded-host header can be an array (`string | string[] | undefined`), which used to be casted to `string | undefined`. So when comparing the origin vs the x-forwarded-host, it ends up comparing an array to a string. Resulting in the following error; ``` `x-forwarded-host` header with value `www.foo.bar, www.foo.bar` does not match `origin` header with value `www.foo.bar` from a forwarded Server Actions request. Aborting the action. ``` --------- Co-authored-by: JJ Kasper <jj@jjsweb.site>
1 parent a07559f commit 41b743a

File tree

3 files changed

+85
-19
lines changed

3 files changed

+85
-19
lines changed
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { parseHostHeader } from './action-handler'
2+
3+
describe('parseHostHeader', () => {
4+
it('should return correct host', () => {
5+
expect(parseHostHeader({})).toBe(undefined)
6+
7+
expect(
8+
parseHostHeader({
9+
host: 'www.foo.com',
10+
})
11+
).toEqual({ type: 'host', value: 'www.foo.com' })
12+
13+
expect(
14+
parseHostHeader({
15+
host: undefined,
16+
'x-forwarded-host': 'www.foo.com',
17+
})
18+
).toEqual({ type: 'x-forwarded-host', value: 'www.foo.com' })
19+
20+
expect(
21+
parseHostHeader({
22+
host: 'www.foo.com',
23+
'x-forwarded-host': undefined,
24+
})
25+
).toEqual({ type: 'host', value: 'www.foo.com' })
26+
})
27+
28+
it('should return x-forwarded-host over host header', () => {
29+
expect(
30+
parseHostHeader({
31+
host: 'www.foo.com',
32+
'x-forwarded-host': 'www.bar.com',
33+
})
34+
).toEqual({ type: 'x-forwarded-host', value: 'www.bar.com' })
35+
})
36+
37+
it('should return correct x-forwarded-host when provided in array', () => {
38+
expect(
39+
parseHostHeader({
40+
host: 'www.foo.com',
41+
'x-forwarded-host': ['www.bar.com', 'www.baz.com'],
42+
})
43+
).toEqual({ type: 'x-forwarded-host', value: 'www.bar.com' })
44+
45+
expect(
46+
parseHostHeader({
47+
host: 'www.foo.com',
48+
'x-forwarded-host': [],
49+
})
50+
).toEqual({ type: 'host', value: 'www.foo.com' })
51+
52+
expect(
53+
parseHostHeader({
54+
host: 'www.foo.com',
55+
'x-forwarded-host': 'www.bar.com, www.baz.com',
56+
})
57+
).toEqual({ type: 'x-forwarded-host', value: 'www.bar.com' })
58+
})
59+
})

packages/next/src/server/app-render/action-handler.ts

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { IncomingHttpHeaders, OutgoingHttpHeaders } from 'http'
1+
import type { IncomingHttpHeaders, OutgoingHttpHeaders } from 'node:http'
22
import type { SizeLimit } from '../../types'
33
import type { RequestStore } from '../app-render/work-unit-async-storage.external'
44
import type { AppRenderContext, GenerateFlight } from './app-render'
@@ -399,6 +399,28 @@ function limitUntrustedHeaderValueForLogs(value: string) {
399399
return value.length > 100 ? value.slice(0, 100) + '...' : value
400400
}
401401

402+
export function parseHostHeader(headers: IncomingHttpHeaders) {
403+
const forwardedHostHeader = headers['x-forwarded-host']
404+
const forwardedHostHeaderValue =
405+
forwardedHostHeader && Array.isArray(forwardedHostHeader)
406+
? forwardedHostHeader[0]
407+
: forwardedHostHeader?.split(',')?.[0]?.trim()
408+
const hostHeader = headers['host']
409+
const host = forwardedHostHeaderValue
410+
? {
411+
type: HostType.XForwardedHost,
412+
value: forwardedHostHeaderValue,
413+
}
414+
: hostHeader
415+
? {
416+
type: HostType.Host,
417+
value: hostHeader,
418+
}
419+
: undefined
420+
421+
return host
422+
}
423+
402424
type ServerModuleMap = Record<
403425
string,
404426
{
@@ -487,22 +509,7 @@ export async function handleAction({
487509
typeof req.headers['origin'] === 'string'
488510
? new URL(req.headers['origin']).host
489511
: undefined
490-
491-
const forwardedHostHeader = req.headers['x-forwarded-host'] as
492-
| string
493-
| undefined
494-
const hostHeader = req.headers['host']
495-
const host: Host = forwardedHostHeader
496-
? {
497-
type: HostType.XForwardedHost,
498-
value: forwardedHostHeader,
499-
}
500-
: hostHeader
501-
? {
502-
type: HostType.Host,
503-
value: hostHeader,
504-
}
505-
: undefined
512+
const host = parseHostHeader(req.headers)
506513

507514
let warning: string | undefined = undefined
508515

packages/next/src/server/base-http/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,14 +67,14 @@ export abstract class BaseNextResponse<Destination = any> {
6767
abstract appendHeader(name: string, value: string): this
6868

6969
/**
70-
* Get all vaues for a header as an array or undefined if no value is present
70+
* Get all values for a header as an array or undefined if no value is present
7171
*/
7272
abstract getHeaderValues(name: string): string[] | undefined
7373

7474
abstract hasHeader(name: string): boolean
7575

7676
/**
77-
* Get vaues for a header concatenated using `,` or undefined if no value is present
77+
* Get values for a header concatenated using `,` or undefined if no value is present
7878
*/
7979
abstract getHeader(name: string): string | undefined
8080

0 commit comments

Comments
 (0)