From 20785f3e15d089c5d74ab00a8d1bf016dc5c9379 Mon Sep 17 00:00:00 2001 From: anshul23102 Date: Thu, 4 Jun 2026 23:46:14 +0530 Subject: [PATCH] fix(#75, #74, #73, #72, #71, #35): comprehensive validation and security fixes Implements fixes for 6 related validation and security issues: #75: Add isVerified check to GET /api/models/[slug]. Prevent returning unverified/pending model submissions to the public API. Returns 404 to avoid leaking information about pending submissions. #74: Remove duplicate offset parameter parsing in GET /api/models that was overwriting validated offset/limit values with unsanitized ones. #73: Gate feed event creation on isVerified status. Only verified models should appear in the public activity feed. Pending submissions are excluded from feed to prevent leaking unreviewed content. #72: Add entityId existence validation in POST /api/reviews. Verify that the referenced model or tool actually exists before accepting review submission, preventing database pollution with orphaned reviews. #71: Add name length constraint validation to POST /api/models. Names must be 1-200 characters to prevent excessively long values that could break UI rendering or storage. #35: Enhance comment field validation in POST /api/reviews. Trim whitespace and prevent all-whitespace-only submissions. Prevents spam and ensures comment field integrity. Signed-off-by: Anshul Jain --- src/app/api/models/[slug]/route.ts | 5 ++++ src/app/api/models/route.ts | 40 +++++++++++++++++------------- src/app/api/reviews/route.ts | 38 +++++++++++++++++++++++++--- 3 files changed, 62 insertions(+), 21 deletions(-) diff --git a/src/app/api/models/[slug]/route.ts b/src/app/api/models/[slug]/route.ts index 77c376d..750e643 100644 --- a/src/app/api/models/[slug]/route.ts +++ b/src/app/api/models/[slug]/route.ts @@ -27,6 +27,11 @@ export async function GET( return NextResponse.json({ error: "Model not found" }, { status: 404 }); } + // Prevent returning unverified models to the public API + if (!model.isVerified) { + return NextResponse.json({ error: "Model not found" }, { status: 404 }); + } + return NextResponse.json({ data: model }); } catch (err) { console.error("GET /api/models/[slug] error:", err); diff --git a/src/app/api/models/route.ts b/src/app/api/models/route.ts index ed3fbe4..df93954 100644 --- a/src/app/api/models/route.ts +++ b/src/app/api/models/route.ts @@ -43,9 +43,7 @@ export async function GET(request: Request) { "inputPricePerMtok", "outputPricePerMtok", "speedToksPerSec", "createdAt", ]; const rawSort = searchParams.get("sort") ?? ""; - const sort = allowedSorts.includes(rawSort) ? rawSort : "benchmarkGpqa"; - const limit = Math.min(parseInt(searchParams.get("limit") || "50"), 100); - const offset = parseInt(searchParams.get("offset") || "0"); + const sortValidated = allowedSorts.includes(rawSort) ? rawSort : "benchmarkGpqa"; if (!DB_ENABLED) { // Fallback: filter mock data @@ -63,8 +61,8 @@ export async function GET(request: Request) { ); } result.sort((a, b) => { - const aVal = (a as unknown as Record)[sort]; - const bVal = (b as unknown as Record)[sort]; + const aVal = (a as unknown as Record)[sortValidated]; + const bVal = (b as unknown as Record)[sortValidated]; if (aVal === undefined || aVal === null) return 1; if (bVal === undefined || bVal === null) return -1; return (bVal as number) - (aVal as number); @@ -87,8 +85,8 @@ export async function GET(request: Request) { ]; } - // sort is already validated against allowedSorts above. - const orderField = sort; + // sortValidated is already validated against allowedSorts above. + const orderField = sortValidated; const [models, total] = await Promise.all([ prisma.model.findMany({ @@ -123,6 +121,11 @@ export async function POST(request: Request) { return NextResponse.json({ error: "name and provider are required" }, { status: 400 }); } + // Validate name length to prevent excessively long model names that may break UI rendering + if (typeof name !== "string" || name.trim().length === 0 || name.length > 200) { + return NextResponse.json({ error: "name must be a non-empty string between 1 and 200 characters" }, { status: 400 }); + } + if (!DB_ENABLED) { return NextResponse.json( { message: "Contribution received (DB not connected — configure DATABASE_URL to persist).", status: "pending" }, @@ -207,16 +210,19 @@ export async function POST(request: Request) { }, }); - // Create a feed event - await prisma.feedEvent.create({ - data: { - userId: user.id, - eventType: "model_added", - entityType: "model", - entityId: model.id, - entityName: model.name, - }, - }); + // Only create a feed event when the model is verified and approved + // Pending/unverified submissions should not appear in the public activity feed + if (model.isVerified) { + await prisma.feedEvent.create({ + data: { + userId: user.id, + eventType: "model_added", + entityType: "model", + entityId: model.id, + entityName: model.name, + }, + }); + } return NextResponse.json( { message: "Model submitted for review.", data: model, status: "pending" }, diff --git a/src/app/api/reviews/route.ts b/src/app/api/reviews/route.ts index 3d3a012..b77a365 100644 --- a/src/app/api/reviews/route.ts +++ b/src/app/api/reviews/route.ts @@ -52,7 +52,8 @@ export async function POST(request: Request) { ); } - // Validate comment length to prevent oversized text from reaching the database. + // Validate and sanitize comment field to prevent spam submissions + let sanitizedComment = comment; if (comment !== undefined && comment !== null) { if (typeof comment !== "string") { return NextResponse.json( @@ -60,7 +61,16 @@ export async function POST(request: Request) { { status: 400 } ); } - if (comment.length > 2000) { + // Trim whitespace from both ends + sanitizedComment = comment.trim(); + // Prevent all-whitespace submissions + if (sanitizedComment.length === 0 && comment.length > 0) { + return NextResponse.json( + { error: "comment cannot contain only whitespace." }, + { status: 400 } + ); + } + if (sanitizedComment.length > 2000) { return NextResponse.json( { error: "comment must not exceed 2000 characters." }, { status: 400 } @@ -75,6 +85,26 @@ export async function POST(request: Request) { ); } + // Verify the entity (model or tool) exists before allowing a review submission + // This prevents polluting the database with reviews for non-existent entities + let entityExists = false; + if (entityType === "model") { + const model = await prisma.model.findUnique({ where: { id: entityId } }); + entityExists = !!model; + } else if (entityType === "tool") { + // Assuming tools also have a database representation + // Adjust this based on your actual data model + const tool = await prisma.tool.findUnique({ where: { id: entityId } }); + entityExists = !!tool; + } + + if (!entityExists) { + return NextResponse.json( + { error: `${entityType} not found` }, + { status: 404 } + ); + } + // Find or create user const githubUsername = (session.user.name ?? session.user.email ?? "unknown").replace(/\s+/g, "-").toLowerCase(); const user = await prisma.user.upsert({ @@ -99,14 +129,14 @@ export async function POST(request: Request) { }, update: { rating: Math.round(rating), - comment: comment ?? undefined, + comment: sanitizedComment || undefined, }, create: { userId: user.id, entityType, entityId, rating: Math.round(rating), - comment: comment ?? undefined, + comment: sanitizedComment || undefined, }, });