teqo-backup/src/utilities/sameOriginRequest.ts

64 lines
2.3 KiB
TypeScript

import 'server-only'
import { getCampaignInviteBaseURL } from '@/utilities/campaignInviteOrigin'
/**
* CSRF guard for cookie-authenticated JSON route handlers (auto-save popovers):
* a same-origin browser request either omits `Origin` (same-site navigation)
* or sends one matching an origin this deployment answers as. Two bases are
* accepted, and both must hold for every consumer:
*
* - the literal `request.url` base — what a browser sends when it reaches this
* server directly (dev, worktrees, direct TLS);
* - the proxy-aware public base resolved with the established
* `getCampaignInviteBaseURL` policy (`x-forwarded-*` for local authorities,
* `NEXT_PUBLIC_SITE_URL` — HTTPS public DNS or throw — in production). This
* is what a browser sends behind the Cloudflare tunnel, where `request.url`
* still carries the internal `http://localhost:3000` origin.
*
* Resolving the second base can throw (production misconfiguration); the
* failure is contained to that base so the decision falls to the first.
* Anything that matches neither is rejected before the body is even parsed.
*
* The `Origin` under verification doubles as the resolver's `requestOrigin`
* input: production ignores it (the configured site URL decides), while
* dev/test accept any localhost/127.0.0.1/[::1] port through it — a dev-only
* relaxation, never a production one.
*
* Every `POST /campanha/**` endpoint inherits this through
* `campaignJsonMutationRoute`; the two remaining JSON routes
* (`campanha/api/ai-transcribe`, `api/social-feed/sync`) call it directly and
* keep their own rejection envelopes.
*/
export const isSameOriginRequest = (request: Request): boolean => {
const origin = request.headers.get('origin')
if (!origin) return true
let originURL: URL
try {
originURL = new URL(origin)
} catch {
return false
}
try {
if (originURL.origin === new URL(request.url).origin) return true
} catch {
return false
}
try {
return (
originURL.origin ===
new URL(
getCampaignInviteBaseURL({
requestOrigin: origin,
forwardedHost: request.headers.get('x-forwarded-host') ?? request.headers.get('host'),
forwardedProto: request.headers.get('x-forwarded-proto'),
}),
).origin
)
} catch {
return false
}
}