From 5e120efe286999882466c37fae80c74d84fcf187 Mon Sep 17 00:00:00 2001 From: Jorel97 <83238249+Jorel97@users.noreply.github.com> Date: Fri, 29 May 2026 14:14:06 -0600 Subject: [PATCH 1/2] fix(marketplace): clamp listing page queries --- src/app/api/mcp/route.test.ts | 42 ++++++++++++++++++++++++++++++++ src/app/api/mcp/route.ts | 7 +++++- src/app/api/skills/route.test.ts | 42 ++++++++++++++++++++++++++++++++ src/app/api/skills/route.ts | 7 +++++- 4 files changed, 96 insertions(+), 2 deletions(-) diff --git a/src/app/api/mcp/route.test.ts b/src/app/api/mcp/route.test.ts index 21251f8a..a7275795 100644 --- a/src/app/api/mcp/route.test.ts +++ b/src/app/api/mcp/route.test.ts @@ -108,6 +108,48 @@ describe("GET /api/mcp", () => { expect(json.listings).toHaveLength(0); expect(json.total).toBe(0); }); + + it("clamps invalid page values to the first page", async () => { + const range = vi.fn(() => + Promise.resolve({ data: [], count: 0, error: null }) + ); + + mockFrom.mockReturnValue({ + select: () => ({ + eq: () => ({ + order: () => ({ range }), + }), + }), + }); + + const response = await GET(makeGetRequest({ page: "-1" })); + const json = await response.json(); + + expect(response.status).toBe(200); + expect(range).toHaveBeenCalledWith(0, 19); + expect(json.page).toBe(1); + }); + + it("truncates fractional page values before calculating ranges", async () => { + const range = vi.fn(() => + Promise.resolve({ data: [], count: 0, error: null }) + ); + + mockFrom.mockReturnValue({ + select: () => ({ + eq: () => ({ + order: () => ({ range }), + }), + }), + }); + + const response = await GET(makeGetRequest({ page: "2.9" })); + const json = await response.json(); + + expect(response.status).toBe(200); + expect(range).toHaveBeenCalledWith(20, 39); + expect(json.page).toBe(2); + }); }); describe("POST /api/mcp", () => { diff --git a/src/app/api/mcp/route.ts b/src/app/api/mcp/route.ts index c85294ba..50e7e58b 100644 --- a/src/app/api/mcp/route.ts +++ b/src/app/api/mcp/route.ts @@ -6,6 +6,11 @@ import { mcpListingSchema, slugify } from "@/lib/mcp/validation"; import { combinedScan, MCP_SCANNER_VERSION } from "@/lib/mcp/security-scan"; import { sanitizeSearchParams } from "@/lib/security/sanitize"; +function parsePage(value: string | null) { + const parsed = Number(value || "1"); + return Number.isFinite(parsed) ? Math.max(1, Math.trunc(parsed)) : 1; +} + /** * GET /api/mcp - Public listing of active MCP servers */ @@ -16,7 +21,7 @@ export async function GET(request: NextRequest) { const category = url.searchParams.get("category") || ""; const tag = sanitizeSearchParams(url, "tag"); const sort = url.searchParams.get("sort") || "newest"; - const page = parseInt(url.searchParams.get("page") || "1"); + const page = parsePage(url.searchParams.get("page")); const limit = 20; const offset = (page - 1) * limit; diff --git a/src/app/api/skills/route.test.ts b/src/app/api/skills/route.test.ts index 3b0b1e5c..02fa4531 100644 --- a/src/app/api/skills/route.test.ts +++ b/src/app/api/skills/route.test.ts @@ -122,6 +122,48 @@ describe("GET /api/skills", () => { expect(json.total).toBe(1); expect(json.page).toBe(1); }); + + it("clamps invalid page values to the first page", async () => { + const range = vi.fn(() => + Promise.resolve({ data: [], count: 0, error: null }) + ); + + mockFrom.mockReturnValue({ + select: () => ({ + eq: () => ({ + order: () => ({ range }), + }), + }), + }); + + const response = await GET(makeGetRequest({ page: "-1" })); + const json = await response.json(); + + expect(response.status).toBe(200); + expect(range).toHaveBeenCalledWith(0, 19); + expect(json.page).toBe(1); + }); + + it("truncates fractional page values before calculating ranges", async () => { + const range = vi.fn(() => + Promise.resolve({ data: [], count: 0, error: null }) + ); + + mockFrom.mockReturnValue({ + select: () => ({ + eq: () => ({ + order: () => ({ range }), + }), + }), + }); + + const response = await GET(makeGetRequest({ page: "2.9" })); + const json = await response.json(); + + expect(response.status).toBe(200); + expect(range).toHaveBeenCalledWith(20, 39); + expect(json.page).toBe(2); + }); }); describe("POST /api/skills", () => { diff --git a/src/app/api/skills/route.ts b/src/app/api/skills/route.ts index d430dda9..594314c0 100644 --- a/src/app/api/skills/route.ts +++ b/src/app/api/skills/route.ts @@ -8,6 +8,11 @@ import { importSkillFromUrl } from "@/lib/skills/url-import"; import { isScanAcceptable } from "@/lib/skills/security-scan"; import { sanitizeSearchParams } from "@/lib/security/sanitize"; +function parsePage(value: string | null) { + const parsed = Number(value || "1"); + return Number.isFinite(parsed) ? Math.max(1, Math.trunc(parsed)) : 1; +} + /** * Detect whether a submission looks like an MCP server listing based on title/tags. */ @@ -32,7 +37,7 @@ export async function GET(request: NextRequest) { const category = url.searchParams.get("category") || ""; const tag = sanitizeSearchParams(url, "tag"); const sort = url.searchParams.get("sort") || "newest"; - const page = parseInt(url.searchParams.get("page") || "1"); + const page = parsePage(url.searchParams.get("page")); const limit = 20; const offset = (page - 1) * limit; From 53cf3f6968579119cd32f1f8e1f89c3fa1284cc1 Mon Sep 17 00:00:00 2001 From: Jorel97 <83238249+Jorel97@users.noreply.github.com> Date: Fri, 29 May 2026 14:18:20 -0600 Subject: [PATCH 2/2] fix(marketplace): cap listing page bounds --- src/app/api/mcp/route.test.ts | 21 +++++++++++++++++++++ src/app/api/mcp/route.ts | 6 +++++- src/app/api/skills/route.test.ts | 21 +++++++++++++++++++++ src/app/api/skills/route.ts | 6 +++++- 4 files changed, 52 insertions(+), 2 deletions(-) diff --git a/src/app/api/mcp/route.test.ts b/src/app/api/mcp/route.test.ts index a7275795..6f76f810 100644 --- a/src/app/api/mcp/route.test.ts +++ b/src/app/api/mcp/route.test.ts @@ -150,6 +150,27 @@ describe("GET /api/mcp", () => { expect(range).toHaveBeenCalledWith(20, 39); expect(json.page).toBe(2); }); + + it("caps huge page values before calculating ranges", async () => { + const range = vi.fn(() => + Promise.resolve({ data: [], count: 0, error: null }) + ); + + mockFrom.mockReturnValue({ + select: () => ({ + eq: () => ({ + order: () => ({ range }), + }), + }), + }); + + const response = await GET(makeGetRequest({ page: "1e308" })); + const json = await response.json(); + + expect(response.status).toBe(200); + expect(range).toHaveBeenCalledWith(1999980, 1999999); + expect(json.page).toBe(100000); + }); }); describe("POST /api/mcp", () => { diff --git a/src/app/api/mcp/route.ts b/src/app/api/mcp/route.ts index 50e7e58b..568629c6 100644 --- a/src/app/api/mcp/route.ts +++ b/src/app/api/mcp/route.ts @@ -6,9 +6,13 @@ import { mcpListingSchema, slugify } from "@/lib/mcp/validation"; import { combinedScan, MCP_SCANNER_VERSION } from "@/lib/mcp/security-scan"; import { sanitizeSearchParams } from "@/lib/security/sanitize"; +const MAX_PAGE = 100_000; + function parsePage(value: string | null) { const parsed = Number(value || "1"); - return Number.isFinite(parsed) ? Math.max(1, Math.trunc(parsed)) : 1; + return Number.isFinite(parsed) + ? Math.min(Math.max(1, Math.trunc(parsed)), MAX_PAGE) + : 1; } /** diff --git a/src/app/api/skills/route.test.ts b/src/app/api/skills/route.test.ts index 02fa4531..e7e07cf4 100644 --- a/src/app/api/skills/route.test.ts +++ b/src/app/api/skills/route.test.ts @@ -164,6 +164,27 @@ describe("GET /api/skills", () => { expect(range).toHaveBeenCalledWith(20, 39); expect(json.page).toBe(2); }); + + it("caps huge page values before calculating ranges", async () => { + const range = vi.fn(() => + Promise.resolve({ data: [], count: 0, error: null }) + ); + + mockFrom.mockReturnValue({ + select: () => ({ + eq: () => ({ + order: () => ({ range }), + }), + }), + }); + + const response = await GET(makeGetRequest({ page: "1e308" })); + const json = await response.json(); + + expect(response.status).toBe(200); + expect(range).toHaveBeenCalledWith(1999980, 1999999); + expect(json.page).toBe(100000); + }); }); describe("POST /api/skills", () => { diff --git a/src/app/api/skills/route.ts b/src/app/api/skills/route.ts index 594314c0..21ae9c76 100644 --- a/src/app/api/skills/route.ts +++ b/src/app/api/skills/route.ts @@ -8,9 +8,13 @@ import { importSkillFromUrl } from "@/lib/skills/url-import"; import { isScanAcceptable } from "@/lib/skills/security-scan"; import { sanitizeSearchParams } from "@/lib/security/sanitize"; +const MAX_PAGE = 100_000; + function parsePage(value: string | null) { const parsed = Number(value || "1"); - return Number.isFinite(parsed) ? Math.max(1, Math.trunc(parsed)) : 1; + return Number.isFinite(parsed) + ? Math.min(Math.max(1, Math.trunc(parsed)), MAX_PAGE) + : 1; } /**