diff --git a/packages/next/src/server/app-render/action-handler.ts b/packages/next/src/server/app-render/action-handler.ts index 1c9a998b9ac2d..84fe9ccbf2c60 100644 --- a/packages/next/src/server/app-render/action-handler.ts +++ b/packages/next/src/server/app-render/action-handler.ts @@ -208,6 +208,29 @@ async function createRedirectRenderResult( return new RenderResult(JSON.stringify({})) } +// Used to compare Host header and Origin header. +const enum HostType { + XForwardedHost = 'x-forwarded-host', + Host = 'host', +} +type Host = + | { + type: HostType.XForwardedHost + value: string + } + | { + type: HostType.Host + value: string + } + | undefined + +/** + * Ensures the value of the header can't create long logs. + */ +function limitUntrustedHeaderValueForLogs(value: string) { + return value.length > 100 ? value.slice(0, 100) + '...' : value +} + export async function handleAction({ req, res, @@ -269,7 +292,22 @@ export async function handleAction({ typeof req.headers['origin'] === 'string' ? new URL(req.headers['origin']).host : undefined - const host = req.headers['x-forwarded-host'] || req.headers['host'] + + const forwardedHostHeader = req.headers['x-forwarded-host'] as + | string + | undefined + const hostHeader = req.headers['host'] + const host: Host = forwardedHostHeader + ? { + type: HostType.XForwardedHost, + value: forwardedHostHeader, + } + : hostHeader + ? { + type: HostType.Host, + value: hostHeader, + } + : undefined // This is to prevent CSRF attacks. If `x-forwarded-host` is set, we need to // ensure that the request is coming from the same host. @@ -279,19 +317,31 @@ export async function handleAction({ console.warn( 'Missing `origin` header from a forwarded Server Actions request.' ) - } else if (!host || originHostname !== host) { + } else if (!host || originHostname !== host.value) { // If the customer sets a list of allowed hosts, we'll allow the request. // These can be their reverse proxies or other safe hosts. if ( - typeof host === 'string' && - serverActions?.allowedForwardedHosts?.includes(host) + host && + typeof host.value === 'string' && + serverActions?.allowedForwardedHosts?.includes(host.value) ) { // Ignore it } else { - // This is an attack. We should not proceed the action. - console.error( - '`x-forwarded-host` and `host` headers do not match `origin` header from a forwarded Server Actions request. Aborting the action.' - ) + if (host) { + // This is an attack. We should not proceed the action. + console.error( + `\`${!host.type}\` header with value \`${limitUntrustedHeaderValueForLogs( + host.value + )}\` does not match \`origin\` header with value \`${limitUntrustedHeaderValueForLogs( + originHostname + )}\` from a forwarded Server Actions request. Aborting the action.` + ) + } else { + // This is an attack. We should not proceed the action. + console.error( + `\`x-forwarded-host\` or \`host\` headers are not provided. One of these is needed to compare the \`origin\` header from a forwarded Server Actions request. Aborting the action.` + ) + } const error = new Error('Invalid Server Actions request.')