diff --git a/apps/web/app/api/webhooks/media-server/progress/route.ts b/apps/web/app/api/webhooks/media-server/progress/route.ts index 57a1ba79dce..2248e07a22d 100644 --- a/apps/web/app/api/webhooks/media-server/progress/route.ts +++ b/apps/web/app/api/webhooks/media-server/progress/route.ts @@ -1,3 +1,4 @@ +import { createHash, timingSafeEqual } from "node:crypto"; import { db } from "@cap/database"; import { videos, videoUploads } from "@cap/database/schema"; import { serverEnv } from "@cap/env"; @@ -67,7 +68,17 @@ export async function POST(request: NextRequest) { try { const webhookSecret = serverEnv().MEDIA_SERVER_WEBHOOK_SECRET; const authHeader = request.headers.get("x-media-server-secret"); - if (!webhookSecret || authHeader !== webhookSecret) { + // Hash both sides to a fixed-length digest before the constant-time + // compare. This avoids comparing raw inputs whose UTF-8 byte length can + // differ from their UTF-16 `.length` (which would throw a RangeError) and + // removes the length pre-check that would otherwise leak the secret size. + const digest = (value: string) => + createHash("sha256").update(value, "utf8").digest(); + if ( + !webhookSecret || + !authHeader || + !timingSafeEqual(digest(authHeader), digest(webhookSecret)) + ) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); }