Skip to content

Commit

Permalink
Improve CSRF protection error in Server Actions (#57980)
Browse files Browse the repository at this point in the history
Follow-up to #57529, this adds extra logging so that you know what the header value was in order to configure it correctly.
  • Loading branch information
timneutkens authored Nov 3, 2023
1 parent 7cea497 commit 4a89feb
Showing 1 changed file with 58 additions and 8 deletions.
66 changes: 58 additions & 8 deletions packages/next/src/server/app-render/action-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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.
Expand All @@ -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.')

Expand Down

0 comments on commit 4a89feb

Please sign in to comment.