From 3bcdd8831809ffb5f0e06d5a2d59027318d9ac8c Mon Sep 17 00:00:00 2001 From: Codex Agent Date: Thu, 4 Jun 2026 17:08:26 -0600 Subject: [PATCH 1/2] Escape user search filters --- src/app/api/users/search/route.test.ts | 16 ++++++++++++++++ src/app/api/users/search/route.ts | 4 +++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/app/api/users/search/route.test.ts b/src/app/api/users/search/route.test.ts index bc95c7e8..ca131e70 100644 --- a/src/app/api/users/search/route.test.ts +++ b/src/app/api/users/search/route.test.ts @@ -102,6 +102,22 @@ describe("GET /api/users/search", () => { expect(mockIlike).toHaveBeenCalledWith("username", "cho%"); }); + it("escapes PostgREST wildcard and filter syntax in username search", async () => { + vi.mocked(getAuthContext).mockResolvedValue({ + user: { id: "user-1", authMethod: "session" }, + supabase: supabaseClient as any, + }); + + mockLimit.mockResolvedValue({ data: [], error: null }); + + await GET(makeRequest("/api/users/search?q=ai_agent,pro.v2")); + + expect(mockIlike).toHaveBeenCalledWith( + "username", + String.raw`ai\_agent\,pro\.v2%` + ); + }); + it("does NOT leak email, full_name, or phone", async () => { vi.mocked(getAuthContext).mockResolvedValue({ user: { id: "user-1", authMethod: "session" }, diff --git a/src/app/api/users/search/route.ts b/src/app/api/users/search/route.ts index 4071a0b1..cedb0179 100644 --- a/src/app/api/users/search/route.ts +++ b/src/app/api/users/search/route.ts @@ -1,5 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; import { getAuthContext } from "@/lib/auth/get-user"; +import { escapePostgrestSearchValue } from "@/lib/security/sanitize"; // GET /api/users/search?q=&limit=10 export async function GET(request: NextRequest) { @@ -21,11 +22,12 @@ export async function GET(request: NextRequest) { } const { supabase } = authContext; + const escapedQuery = escapePostgrestSearchValue(query); const { data: users, error } = await supabase .from("profiles") .select("id, username, avatar_url") - .ilike("username", `${query}%`) + .ilike("username", `${escapedQuery}%`) .limit(limit); if (error) { From 3a8c79519eedffbd11cae2367ac79dbb88cdfc6c Mon Sep 17 00:00:00 2001 From: Codex Agent Date: Thu, 4 Jun 2026 20:18:05 -0600 Subject: [PATCH 2/2] Escape user search wildcard aliases --- src/app/api/users/search/route.test.ts | 4 ++-- src/lib/security/sanitize.ts | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/app/api/users/search/route.test.ts b/src/app/api/users/search/route.test.ts index ca131e70..41bcc933 100644 --- a/src/app/api/users/search/route.test.ts +++ b/src/app/api/users/search/route.test.ts @@ -110,11 +110,11 @@ describe("GET /api/users/search", () => { mockLimit.mockResolvedValue({ data: [], error: null }); - await GET(makeRequest("/api/users/search?q=ai_agent,pro.v2")); + await GET(makeRequest("/api/users/search?q=100%*ai_agent,pro.v2")); expect(mockIlike).toHaveBeenCalledWith( "username", - String.raw`ai\_agent\,pro\.v2%` + String.raw`100\%\*ai\_agent\,pro\.v2%` ); }); diff --git a/src/lib/security/sanitize.ts b/src/lib/security/sanitize.ts index 790b3c14..4bcc81be 100644 --- a/src/lib/security/sanitize.ts +++ b/src/lib/security/sanitize.ts @@ -46,8 +46,9 @@ export function sanitizeSearchParams( /** * Escape user text before interpolating it into a PostgREST filter string. * PostgREST uses punctuation such as commas, periods, and parentheses as - * filter syntax, while SQL LIKE treats % and _ as wildcards. + * filter syntax, while SQL LIKE treats % and _ as wildcards. PostgREST also + * accepts * as a % alias in like/ilike filters. */ export function escapePostgrestSearchValue(value: string): string { - return value.replace(/[\\%_,().]/g, (char) => `\\${char}`); + return value.replace(/[\\%*_,().]/g, (char) => `\\${char}`); }