From 9711639f8d6f2ed6d6c4489556ce835a955248a2 Mon Sep 17 00:00:00 2001 From: dirtybits Date: Sun, 6 Sep 2026 09:48:53 -0700 Subject: [PATCH] fix(web): handle malformed skill route params --- .../lib/skill-route-resolver.test.ts | 30 +++++++++++++++++++ web/lib/skillRouteResolver.ts | 17 +++++++++-- 2 files changed, 44 insertions(+), 3 deletions(-) create mode 100644 web/__tests__/lib/skill-route-resolver.test.ts diff --git a/web/__tests__/lib/skill-route-resolver.test.ts b/web/__tests__/lib/skill-route-resolver.test.ts new file mode 100644 index 00000000..74754053 --- /dev/null +++ b/web/__tests__/lib/skill-route-resolver.test.ts @@ -0,0 +1,30 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const { mockInitializeDatabase, mockSql } = vi.hoisted(() => ({ + mockInitializeDatabase: vi.fn(), + mockSql: vi.fn(), +})); + +vi.mock("@/lib/db", () => ({ + initializeDatabase: mockInitializeDatabase, + sql: () => mockSql, +})); + +import { + resolveSkillRouteParam, + resolveSkillRoutePath, +} from "@/lib/skillRouteResolver"; + +describe("skill route resolution", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("treats malformed percent-encoded route params as missing before database work", async () => { + await expect(resolveSkillRoutePath("%", "skill")).resolves.toBeNull(); + await expect(resolveSkillRouteParam("%")).resolves.toBeNull(); + + expect(mockInitializeDatabase).not.toHaveBeenCalled(); + expect(mockSql).not.toHaveBeenCalled(); + }); +}); diff --git a/web/lib/skillRouteResolver.ts b/web/lib/skillRouteResolver.ts index 7a5d2d2a..84692e8a 100644 --- a/web/lib/skillRouteResolver.ts +++ b/web/lib/skillRouteResolver.ts @@ -20,6 +20,14 @@ export function getCanonicalSkillPath(skill: SkillRouteRecord): string { return getPublicSkillPath(skill); } +function decodeRouteParam(rawParam: string): string | null { + try { + return decodeURIComponent(rawParam); + } catch { + return null; + } +} + export async function buildUniquePublicSkillRoute( db: SqlQuery, input: { @@ -73,9 +81,11 @@ export const resolveSkillRoutePath = cache( rawAuthorSlug: string, rawSkillSlug: string ): Promise => { + const authorSlug = decodeRouteParam(rawAuthorSlug); + const skillSlug = decodeRouteParam(rawSkillSlug); + if (authorSlug === null || skillSlug === null) return null; + await initializeDatabase(); - const authorSlug = decodeURIComponent(rawAuthorSlug); - const skillSlug = decodeURIComponent(rawSkillSlug); const rows = await sql()` SELECT id, skill_id, public_slug, public_author_slug FROM skills @@ -120,7 +130,8 @@ export async function listStaticSkillRouteParams( export async function resolveSkillRouteParam( rawParam: string ): Promise { - const param = decodeURIComponent(rawParam); + const param = decodeRouteParam(rawParam); + if (param === null) return null; if (param.startsWith(CHAIN_SKILL_PREFIX)) { return { id: param,