fix: derive redirect and origin check from Host header

req.url resolves to the internal hostname in Docker standalone mode.
Read the Host header directly so redirects and CSRF origin checks use
whatever host the browser actually used (IP, hostname, or domain).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
jerick
2026-04-20 16:44:11 -04:00
parent 8b0fba5014
commit 2e264014b6

View File

@@ -33,10 +33,22 @@ function isRateLimited(ip: string): boolean {
function hasValidOrigin(req: NextRequest): boolean { function hasValidOrigin(req: NextRequest): boolean {
const origin = req.headers.get('origin') const origin = req.headers.get('origin')
if (!origin) return true // non-browser (curl, server-to-server) if (!origin) return true // non-browser (curl, server-to-server)
const expected = process.env.NEXTAUTH_URL // Accept the host the browser actually used to reach this server.
? new URL(process.env.NEXTAUTH_URL).origin const host = req.headers.get('x-forwarded-host') ?? req.headers.get('host')
: req.nextUrl.origin const proto = req.headers.get('x-forwarded-proto') ?? 'http'
return origin === expected const requestOrigin = host ? `${proto}://${host}` : null
if (requestOrigin && origin === requestOrigin) return true
// Also accept the statically configured NEXTAUTH_URL origin (reverse-proxy setups).
if (process.env.NEXTAUTH_URL && origin === new URL(process.env.NEXTAUTH_URL).origin) return true
return false
}
// Build an absolute URL using the Host header the browser sent, not the
// internal hostname Next.js resolves to inside Docker.
function siteUrl(req: NextRequest, path: string): URL {
const host = req.headers.get('x-forwarded-host') ?? req.headers.get('host') ?? 'localhost:3000'
const proto = req.headers.get('x-forwarded-proto') ?? 'http'
return new URL(path, `${proto}://${host}`)
} }
export default auth((req) => { export default auth((req) => {
@@ -72,13 +84,13 @@ export default auth((req) => {
if (pathname.startsWith('/api/')) { if (pathname.startsWith('/api/')) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
} }
const loginUrl = new URL('/login', req.url) const loginUrl = siteUrl(req, '/login')
loginUrl.searchParams.set('callbackUrl', pathname) loginUrl.searchParams.set('callbackUrl', pathname)
return NextResponse.redirect(loginUrl) return NextResponse.redirect(loginUrl)
} }
if (pathname === '/login') { if (pathname === '/login') {
return NextResponse.redirect(new URL('/dashboard', req.url)) return NextResponse.redirect(siteUrl(req, '/dashboard'))
} }
return NextResponse.next() return NextResponse.next()