fix(proxy): trust forwarded Host header for CSRF origin check
The previous attempt compared the Origin host against request.nextUrl.host, but behind the custom-server + reverse-proxy setup nextUrl.host does NOT resolve to the public host (mutations stayed 403 in prod). Accept the Origin/Referer host if it matches ANY of: the forwarded Host header (nginx sets `proxy_set_header Host $host` → crm.portnimara.com), APP_URL's host, or nextUrl.host. The Host header is the reliable source here. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
17
src/proxy.ts
17
src/proxy.ts
@@ -132,7 +132,6 @@ function originAllowed(request: NextRequest): boolean {
|
|||||||
// mutation in production. The Host header is preserved across the proxy,
|
// mutation in production. The Host header is preserved across the proxy,
|
||||||
// and a matching host is what same-origin CSRF defense actually needs
|
// and a matching host is what same-origin CSRF defense actually needs
|
||||||
// (a cross-site attacker can't forge the browser-set Origin host).
|
// (a cross-site attacker can't forge the browser-set Origin host).
|
||||||
const expectedHost = request.nextUrl.host;
|
|
||||||
const hostOf = (value: string | null): string | null => {
|
const hostOf = (value: string | null): string | null => {
|
||||||
if (!value) return null;
|
if (!value) return null;
|
||||||
try {
|
try {
|
||||||
@@ -141,8 +140,20 @@ function originAllowed(request: NextRequest): boolean {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if (origin) return hostOf(origin) === expectedHost;
|
// Acceptable hosts. Behind the TLS-terminating proxy request.nextUrl.host
|
||||||
if (referer) return hostOf(referer) === expectedHost;
|
// can be the upstream bind (127.0.0.1:PORT) rather than the public host,
|
||||||
|
// so it can't be the sole source of truth. The Host header is forwarded
|
||||||
|
// verbatim by nginx (`proxy_set_header Host $host`), and APP_URL is the
|
||||||
|
// canonical configured origin — trust those too. Comparing hosts (not
|
||||||
|
// full origins) is intentional: TLS terminates upstream so the protocol
|
||||||
|
// is unreliable, and a matching host is what CSRF defense needs.
|
||||||
|
const allowedHosts = new Set(
|
||||||
|
[request.headers.get('host'), hostOf(process.env.APP_URL ?? null), request.nextUrl.host].filter(
|
||||||
|
(h): h is string => Boolean(h),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const candidate = origin ? hostOf(origin) : referer ? hostOf(referer) : null;
|
||||||
|
if (candidate !== null) return allowedHosts.has(candidate);
|
||||||
// Neither header present: most browser fetches always send Origin on
|
// Neither header present: most browser fetches always send Origin on
|
||||||
// POST/PUT/PATCH/DELETE, so this likely means a same-origin server-side
|
// POST/PUT/PATCH/DELETE, so this likely means a same-origin server-side
|
||||||
// call (e.g. Next.js internal fetch). Allow.
|
// call (e.g. Next.js internal fetch). Allow.
|
||||||
|
|||||||
Reference in New Issue
Block a user