003ReviewseniorTypeScript / Next.js真实来源改编 Next.js
Server Actions:支持登录后跳回原页面的相对路径重定向
审查一个 Server Action 补丁,作者让它只接受相对路径 redirect,并基于当前请求的 origin 构造目标 URL 后回取页面内容。
@@ -3,15 +3,20 @@ import { headers } from 'next/headers' -const APP_ORIGIN = process.env.APP_ORIGIN ?? 'https://app.example.com'--// Resolve the redirect target against the trusted app origin, then fetch-// the page on the server so the action can return its rendered contents. export async function followActionRedirect(redirectUrl: string) {- // Relative paths resolve against APP_ORIGIN; absolute URLs keep their own- // origin and are rejected below.- const target = new URL(redirectUrl, APP_ORIGIN)-- if (target.origin !== APP_ORIGIN) {- throw new Error('cross-origin redirect is not allowed')- }+ // Only accept relative paths so we never follow a redirect off to another+ // site; absolute URLs are rejected outright.+ if (!redirectUrl.startsWith('/')) {+ throw new Error('Only relative redirects are allowed')+ }++ // Rebuild the target on the user's current site so multi-domain and+ // preview deployments redirect back to the right origin.+ const h = headers()+ const proto = h.get('x-forwarded-proto') ?? 'https'+ const host = h.get('host')+ const target = new URL(proto + '://' + host + redirectUrl) const response = await fetch(target, { headers: { 'rsc-action': '1' } }) return response.text() }@@ -6,6 +6,13 @@ test('rejects absolute redirect urls', async () => { await expect(followActionRedirect('https://evil.test/x')).rejects.toThrow() }) +test('allows relative redirect back to the original page', async () => {+ setRequestHeaders({ host: 'app.example.com', 'x-forwarded-proto': 'https' })+ await expect(followActionRedirect('/dashboard')).resolves.toContain('ok')+})+ test('rejects paths that are not relative', async () => { await expect(followActionRedirect('mailto:x@evil.test')).rejects.toThrow() })