From 5953b733603cce5c1b9ad6c80f835104d5483f5a Mon Sep 17 00:00:00 2001 From: Jorel97 <83238249+Jorel97@users.noreply.github.com> Date: Fri, 29 May 2026 14:27:37 -0600 Subject: [PATCH] fix(search): clamp pagination bounds --- src/app/api/search/route.test.ts | 29 +++++++++++++++++++++++++++++ src/app/api/search/route.ts | 15 +++++++++++++-- 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/src/app/api/search/route.test.ts b/src/app/api/search/route.test.ts index fe87d82c..c7fe99f9 100644 --- a/src/app/api/search/route.test.ts +++ b/src/app/api/search/route.test.ts @@ -272,6 +272,35 @@ describe("GET /api/search", () => { expect(chain.range).toHaveBeenCalledWith(0, 9); }); + it("truncates fractional page values before calculating ranges", async () => { + const chain = chainResult({ data: [], error: null, count: 0 }); + mockFrom.mockReturnValue(chain); + + const res = await GET( + makeRequest({ q: "test", type: "gigs", page: "2.9", limit: "5" }) + ); + const json = await res.json(); + + expect(res.status).toBe(200); + expect(chain.range).toHaveBeenCalledWith(5, 9); + expect(json.results.gigs.page).toBe(2); + }); + + it("caps huge page values before calculating ranges", async () => { + const chain = chainResult({ data: [], error: null, count: 0 }); + mockFrom.mockReturnValue(chain); + + const res = await GET( + makeRequest({ q: "test", type: "gigs", page: "1e308", limit: "999" }) + ); + const json = await res.json(); + + expect(res.status).toBe(200); + expect(chain.range).toHaveBeenCalledWith(4999950, 4999999); + expect(json.results.gigs.page).toBe(100000); + expect(json.results.gigs.limit).toBe(50); + }); + // ── SQL character escaping ──────────────────────────────────── it("escapes % in search query", async () => { diff --git a/src/app/api/search/route.ts b/src/app/api/search/route.ts index dafa15ff..c42e7f9c 100644 --- a/src/app/api/search/route.ts +++ b/src/app/api/search/route.ts @@ -3,14 +3,25 @@ import { createClient } from "@/lib/supabase/server"; type SearchType = "gigs" | "agents" | "posts" | "all"; +function parsePaginationParam( + value: string | null, + defaultValue: number, + min: number, + max: number +) { + const parsed = Number(value && value.trim() !== "" ? value : defaultValue); + const finiteValue = Number.isFinite(parsed) ? parsed : defaultValue; + return Math.min(Math.max(Math.trunc(finiteValue), min), max); +} + // GET /api/search?q=&type=gigs|agents|posts|all&page=1&limit=10 export async function GET(request: NextRequest) { try { const searchParams = request.nextUrl.searchParams; const query = searchParams.get("q")?.trim() || ""; const type = (searchParams.get("type") || "all") as SearchType; - const page = Math.max(1, Number(searchParams.get("page")) || 1); - const limit = Math.min(50, Math.max(1, Number(searchParams.get("limit")) || 10)); + const page = parsePaginationParam(searchParams.get("page"), 1, 1, 100_000); + const limit = parsePaginationParam(searchParams.get("limit"), 10, 1, 50); if (!query) { return NextResponse.json(