diff --git a/app/api/watch/[videoId]/upload-token/route.ts b/app/api/watch/[videoId]/upload-token/route.ts index 9a12bfc..49f4b62 100644 --- a/app/api/watch/[videoId]/upload-token/route.ts +++ b/app/api/watch/[videoId]/upload-token/route.ts @@ -2,6 +2,7 @@ import { NextRequest } from 'next/server'; import { auth, checkProjectAccess } from '@/lib/auth'; import { db } from '@/lib/db'; import { rateLimit } from '@/lib/rate-limit'; +import { isTrustedSameOriginRequest } from '@/lib/request-origin'; import { validateShareLinkAccess } from '@/lib/share-links'; import { getShareSessionFromRequest } from '@/lib/share-session'; import { apiErrors, successResponse, withCacheControl } from '@/lib/api-response'; @@ -20,7 +21,7 @@ function validateSameOriginRequest(request: NextRequest): Response | null { return apiErrors.forbidden('Missing Origin header'); } - if (origin !== request.nextUrl.origin) { + if (!isTrustedSameOriginRequest(request)) { return apiErrors.forbidden('Cross-origin requests are not allowed'); } diff --git a/app/watch/[videoId]/session/route.ts b/app/watch/[videoId]/session/route.ts index 219cf92..66dbca2 100644 --- a/app/watch/[videoId]/session/route.ts +++ b/app/watch/[videoId]/session/route.ts @@ -2,6 +2,7 @@ import { createHash } from 'crypto'; import { NextRequest, NextResponse } from 'next/server'; import { db } from '@/lib/db'; import { checkRateLimit, getClientIp, rateLimit, rateLimitHeaders } from '@/lib/rate-limit'; +import { isTrustedSameOriginRequest } from '@/lib/request-origin'; import { MAX_SHARE_PASSWORD_LENGTH, validateShareLinkAccess } from '@/lib/share-links'; import { createPendingShareValue, @@ -38,7 +39,7 @@ function validateSameOriginRequest(request: NextRequest): NextResponse | null { return NextResponse.json({ error: 'Missing Origin header' }, { status: 403 }); } - if (origin !== request.nextUrl.origin) { + if (!isTrustedSameOriginRequest(request)) { return NextResponse.json({ error: 'Cross-origin requests are not allowed' }, { status: 403 }); } diff --git a/lib/request-origin.ts b/lib/request-origin.ts new file mode 100644 index 0000000..0bff1ff --- /dev/null +++ b/lib/request-origin.ts @@ -0,0 +1,63 @@ +import type { NextRequest } from 'next/server'; + +function normalizeOrigin(value: string): string | null { + try { + return new URL(value).origin; + } catch { + return null; + } +} + +function getConfiguredOrigins(): string[] { + const configured = [process.env.NEXT_PUBLIC_APP_URL, process.env.NEXTAUTH_URL]; + return configured + .filter((value): value is string => typeof value === 'string' && value.trim().length > 0) + .map((value) => normalizeOrigin(/^https?:\/\//i.test(value) ? value : `https://${value}`)) + .filter((value): value is string => value !== null); +} + +function getForwardedOrigin(request: NextRequest): string | null { + const forwardedProto = request.headers.get('x-forwarded-proto')?.split(',')[0]?.trim(); + const forwardedHost = request.headers.get('x-forwarded-host')?.split(',')[0]?.trim(); + + if (!forwardedProto || !forwardedHost) return null; + return normalizeOrigin(`${forwardedProto}://${forwardedHost}`); +} + +function getHostHeaderOrigin(request: NextRequest): string | null { + const host = request.headers.get('host')?.split(',')[0]?.trim(); + if (!host) return null; + + const forwardedProto = request.headers.get('x-forwarded-proto')?.split(',')[0]?.trim(); + const protocol = forwardedProto || request.nextUrl.protocol.replace(':', ''); + if (!protocol) return null; + + return normalizeOrigin(`${protocol}://${host}`); +} + +export function getAllowedRequestOrigins(request: NextRequest): Set { + const origins = new Set(); + origins.add(request.nextUrl.origin); + + const forwardedOrigin = getForwardedOrigin(request); + if (forwardedOrigin) origins.add(forwardedOrigin); + + const hostHeaderOrigin = getHostHeaderOrigin(request); + if (hostHeaderOrigin) origins.add(hostHeaderOrigin); + + for (const configuredOrigin of getConfiguredOrigins()) { + origins.add(configuredOrigin); + } + + return origins; +} + +export function isTrustedSameOriginRequest(request: NextRequest): boolean { + const requestOrigin = request.headers.get('origin'); + if (!requestOrigin) return false; + + const normalizedRequestOrigin = normalizeOrigin(requestOrigin); + if (!normalizedRequestOrigin) return false; + + return getAllowedRequestOrigins(request).has(normalizedRequestOrigin); +}