From a3447e5dad0882ae098e9f6261cee68894bb359a Mon Sep 17 00:00:00 2001 From: David Krcek Date: Tue, 9 Jun 2026 23:54:09 +0200 Subject: [PATCH] fix(security): add origin allow-list check to zipball proxy route The zipball route was the only GitHub proxy route without the isOriginAllowed() same-origin guard, letting any cross-origin page use the proxy as an egress amplifier for multi-MB repo archives. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/app/api/github/zipball/route.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/app/api/github/zipball/route.ts b/src/app/api/github/zipball/route.ts index 35b6d36e..4c97545c 100644 --- a/src/app/api/github/zipball/route.ts +++ b/src/app/api/github/zipball/route.ts @@ -1,5 +1,6 @@ import { NextResponse } from 'next/server' import { checkRateLimit, getClientIp } from '@/utils/rateLimit' +import { isOriginAllowed } from '@/utils/originAllowlist' // Proxies `GET /repos/{owner}/{repo}/zipball/{ref}`. The endpoint is normally // callable directly from the browser, but GitHub returns a 302 redirect to @@ -13,6 +14,16 @@ import { checkRateLimit, getClientIp } from '@/utils/rateLimit' // // Rate-limited tightly because each call can pull tens of MB. export async function POST(request: Request) { + // Same-origin guard: refuse calls that didn't originate from the noteser + // app itself. Without it this is the only proxy route a cross-origin page + // could use to burn our egress on multi-MB archive downloads. + const origin = isOriginAllowed(request) + if (!origin.ok) { + return NextResponse.json( + { error: 'forbidden', error_description: origin.reason }, + { status: 403 }, + ) + } const limit = checkRateLimit(`zipball:${getClientIp(request)}`, { max: 6, windowMs: 60_000 }) if (!limit.ok) { return NextResponse.json(