003ReviewseniorTypeScript / Next.js真实来源改编 Next.js

Server Actions:支持登录后跳回原页面的相对路径重定向

审查一个 Server Action 补丁,作者让它只接受相对路径 redirect,并基于当前请求的 origin 构造目标 URL 后回取页面内容。

ai-pr.diffdiff · 51 lines
diff --git a/app/actions/redirect.ts b/app/actions/redirect.tsindex 0b7f1a1..bad0003 100644--- a/app/actions/redirect.ts+++ b/app/actions/redirect.ts@@ -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() }diff --git a/app/actions/redirect.test.ts b/app/actions/redirect.test.tsindex 71c0a3e..a9e21b4 100644--- a/app/actions/redirect.test.ts+++ b/app/actions/redirect.test.ts@@ -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() })