From 29847434c8aecac60029e084739011d32d324bbd Mon Sep 17 00:00:00 2001 From: Zekbot001 <280472700+Zekbot001@users.noreply.github.com> Date: Sun, 31 May 2026 15:45:18 +0200 Subject: [PATCH] fix(follows): cap huge public offsets --- .../users/[username]/followers/route.test.ts | 20 +++++++++++++++++++ .../api/users/[username]/followers/route.ts | 8 +++++--- .../users/[username]/following/route.test.ts | 20 +++++++++++++++++++ .../api/users/[username]/following/route.ts | 8 +++++--- 4 files changed, 50 insertions(+), 6 deletions(-) diff --git a/src/app/api/users/[username]/followers/route.test.ts b/src/app/api/users/[username]/followers/route.test.ts index 0b42b4f3..05765af5 100644 --- a/src/app/api/users/[username]/followers/route.test.ts +++ b/src/app/api/users/[username]/followers/route.test.ts @@ -104,4 +104,24 @@ describe("GET /api/users/[username]/followers", () => { expect(follows.range).toHaveBeenCalledWith(30, 39); }); + + it("caps huge offsets before applying range", async () => { + const targetProfile = profileChain({ data: { id: "user-123" }, error: null }); + const follows = followsChain({ data: [], error: null, count: 0 }); + + let callCount = 0; + mockFrom.mockImplementation(() => { + callCount++; + return callCount === 1 ? targetProfile : follows; + }); + + const res = await GET( + makeRequest({ limit: "10", offset: "999999999" }), + routeParams + ); + const json = await res.json(); + + expect(follows.range).toHaveBeenCalledWith(100000, 100009); + expect(json.pagination.offset).toBe(100000); + }); }); diff --git a/src/app/api/users/[username]/followers/route.ts b/src/app/api/users/[username]/followers/route.ts index b9a4adfc..652b7367 100644 --- a/src/app/api/users/[username]/followers/route.ts +++ b/src/app/api/users/[username]/followers/route.ts @@ -1,6 +1,8 @@ import { NextRequest, NextResponse } from "next/server"; import { createClient } from "@/lib/supabase/server"; +const MAX_FOLLOW_OFFSET = 100_000; + function parsePositiveInt(value: string | null, fallback: number, max: number): number { const parsed = Number.parseInt(value ?? "", 10); if (!Number.isFinite(parsed) || parsed <= 0) { @@ -9,12 +11,12 @@ function parsePositiveInt(value: string | null, fallback: number, max: number): return Math.min(parsed, max); } -function parseNonNegativeInt(value: string | null, fallback: number): number { +function parseNonNegativeInt(value: string | null, fallback: number, max: number): number { const parsed = Number.parseInt(value ?? "", 10); if (!Number.isFinite(parsed) || parsed < 0) { return fallback; } - return parsed; + return Math.min(parsed, max); } // GET /api/users/[username]/followers - list a user's followers @@ -27,7 +29,7 @@ export async function GET( const supabase = await createClient(); const searchParams = request.nextUrl.searchParams; const limit = parsePositiveInt(searchParams.get("limit"), 20, 100); - const offset = parseNonNegativeInt(searchParams.get("offset"), 0); + const offset = parseNonNegativeInt(searchParams.get("offset"), 0, MAX_FOLLOW_OFFSET); // Look up target user const { data: targetProfile, error: profileError } = await supabase diff --git a/src/app/api/users/[username]/following/route.test.ts b/src/app/api/users/[username]/following/route.test.ts index ecab1833..7cc77b8b 100644 --- a/src/app/api/users/[username]/following/route.test.ts +++ b/src/app/api/users/[username]/following/route.test.ts @@ -104,4 +104,24 @@ describe("GET /api/users/[username]/following", () => { expect(follows.range).toHaveBeenCalledWith(30, 39); }); + + it("caps huge offsets before applying range", async () => { + const targetProfile = profileChain({ data: { id: "user-123" }, error: null }); + const follows = followsChain({ data: [], error: null, count: 0 }); + + let callCount = 0; + mockFrom.mockImplementation(() => { + callCount++; + return callCount === 1 ? targetProfile : follows; + }); + + const res = await GET( + makeRequest({ limit: "10", offset: "999999999" }), + routeParams + ); + const json = await res.json(); + + expect(follows.range).toHaveBeenCalledWith(100000, 100009); + expect(json.pagination.offset).toBe(100000); + }); }); diff --git a/src/app/api/users/[username]/following/route.ts b/src/app/api/users/[username]/following/route.ts index 855d4ef7..b4d70502 100644 --- a/src/app/api/users/[username]/following/route.ts +++ b/src/app/api/users/[username]/following/route.ts @@ -1,6 +1,8 @@ import { NextRequest, NextResponse } from "next/server"; import { createClient } from "@/lib/supabase/server"; +const MAX_FOLLOW_OFFSET = 100_000; + function parsePositiveInt(value: string | null, fallback: number, max: number): number { const parsed = Number.parseInt(value ?? "", 10); if (!Number.isFinite(parsed) || parsed <= 0) { @@ -9,12 +11,12 @@ function parsePositiveInt(value: string | null, fallback: number, max: number): return Math.min(parsed, max); } -function parseNonNegativeInt(value: string | null, fallback: number): number { +function parseNonNegativeInt(value: string | null, fallback: number, max: number): number { const parsed = Number.parseInt(value ?? "", 10); if (!Number.isFinite(parsed) || parsed < 0) { return fallback; } - return parsed; + return Math.min(parsed, max); } // GET /api/users/[username]/following - list who a user follows @@ -27,7 +29,7 @@ export async function GET( const supabase = await createClient(); const searchParams = request.nextUrl.searchParams; const limit = parsePositiveInt(searchParams.get("limit"), 20, 100); - const offset = parseNonNegativeInt(searchParams.get("offset"), 0); + const offset = parseNonNegativeInt(searchParams.get("offset"), 0, MAX_FOLLOW_OFFSET); // Look up target user const { data: targetProfile, error: profileError } = await supabase