From 7ec7c2445570f890fa0e8a6e07caea3aaa978a32 Mon Sep 17 00:00:00 2001 From: Sumit Kumar Date: Sun, 14 Jun 2026 23:07:02 +0530 Subject: [PATCH 1/2] fix(env-vars): require auth on GET, POST and DELETE handlers All three handlers were missing auth checks, allowing any unauthenticated client to read decrypted secrets or mutate env vars for any project. Added auth() guard at the top of each handler, matching the pattern used in /api/attack-pipeline/route.ts. Closes #120 --- app/api/env-vars/route.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/app/api/env-vars/route.ts b/app/api/env-vars/route.ts index 849c0f6..8f830bf 100644 --- a/app/api/env-vars/route.ts +++ b/app/api/env-vars/route.ts @@ -1,4 +1,5 @@ import { NextResponse } from "next/server"; +import { auth } from "@/lib/auth"; import { getEnvVars, listEnvVars, @@ -12,6 +13,11 @@ import { * otherwise → returns masked values */ export async function GET(request: Request) { + const session = await auth(); + if (!session?.user?.id) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + try { const { searchParams } = new URL(request.url); const project = searchParams.get("project"); @@ -42,6 +48,11 @@ export async function GET(request: Request) { * Body: { project: string; key: string; value: string } */ export async function POST(request: Request) { + const session = await auth(); + if (!session?.user?.id) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + try { const body = await request.json(); const { project, key, value } = body; @@ -74,6 +85,11 @@ export async function POST(request: Request) { * Body: { project: string; key: string } */ export async function DELETE(request: Request) { + const session = await auth(); + if (!session?.user?.id) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + try { const body = await request.json(); const { project, key } = body; From 7cc72a633e70b4bbbd5008b86eb7e20b862aaf3c Mon Sep 17 00:00:00 2001 From: Sumit Kumar Date: Sun, 14 Jun 2026 23:15:15 +0530 Subject: [PATCH 2/2] fix(env-vars): add project ownership check to prevent IDOR After authentication, each handler now verifies the authenticated user owns the requested project by checking for a matching deployment record (user_id + repo_name). Returns 403 if no match is found. Extracted into a shared userOwnsProject() helper used by all three handlers. Addresses CodeRabbit review on PR #133. --- app/api/env-vars/route.ts | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/app/api/env-vars/route.ts b/app/api/env-vars/route.ts index 8f830bf..fd78d08 100644 --- a/app/api/env-vars/route.ts +++ b/app/api/env-vars/route.ts @@ -1,5 +1,6 @@ import { NextResponse } from "next/server"; import { auth } from "@/lib/auth"; +import { getDb, ensureTables } from "@/lib/db"; import { getEnvVars, listEnvVars, @@ -7,6 +8,18 @@ import { deleteEnvVar, } from "@/lib/env-store"; +/** Returns true if userId owns at least one deployment for the given repoName. */ +async function userOwnsProject(userId: string, project: string): Promise { + await ensureTables(); + const sql = getDb(); + const rows = await sql` + SELECT 1 FROM deployments + WHERE user_id = ${userId} AND repo_name = ${project} + LIMIT 1 + `; + return rows.length > 0; +} + /** * GET /api/env-vars?project=&reveal=1 * reveal=1 → returns plaintext values (admin use only) @@ -27,6 +40,10 @@ export async function GET(request: Request) { return NextResponse.json({ error: "project query param required" }, { status: 400 }); } + if (!(await userOwnsProject(session.user.id, project))) { + return NextResponse.json({ error: "Forbidden" }, { status: 403 }); + } + if (reveal) { const vars = await getEnvVars(project); return NextResponse.json({ @@ -64,6 +81,10 @@ export async function POST(request: Request) { ); } + if (!(await userOwnsProject(session.user.id, project))) { + return NextResponse.json({ error: "Forbidden" }, { status: 403 }); + } + // Basic key validation — only allow safe env var names if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) { return NextResponse.json( @@ -98,6 +119,10 @@ export async function DELETE(request: Request) { return NextResponse.json({ error: "project and key are required" }, { status: 400 }); } + if (!(await userOwnsProject(session.user.id, project))) { + return NextResponse.json({ error: "Forbidden" }, { status: 403 }); + } + await deleteEnvVar(project, key); return NextResponse.json({ ok: true }); } catch (err: unknown) {