Skip to content

Commit c8f79ab

Browse files
samejrclaude
andcommitted
Harden URL parseInt with isFinite guards
Adds parseFiniteInt() to webapp searchParams utils. parseInt() accepts garbage-suffixed numbers and returns NaN for non-numeric input, both of which silently nudge time-range and pagination semantics. Apply at the agent, standard, and scheduled task landing-page loaders. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent a2a9b6d commit c8f79ab

4 files changed

Lines changed: 23 additions & 14 deletions

File tree

  • apps/webapp/app
    • routes
      • _app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.agents.$agentParam
      • _app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.tasks.scheduled.$taskParam
      • _app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.tasks.standard.$taskParam
    • utils

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.agents.$agentParam/route.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import {
4848
v3EnvironmentPath,
4949
v3PlaygroundAgentPath,
5050
} from "~/utils/pathBuilder";
51+
import { parseFiniteInt } from "~/utils/searchParams";
5152

5253
export const meta: MetaFunction<typeof loader> = ({ data }) => {
5354
const slug = (data as { agent?: AgentDetail | null } | undefined)?.agent?.slug;
@@ -75,10 +76,8 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
7576

7677
const url = new URL(request.url);
7778
const period = url.searchParams.get("period") ?? undefined;
78-
const fromStr = url.searchParams.get("from");
79-
const toStr = url.searchParams.get("to");
80-
const from = fromStr ? parseInt(fromStr, 10) : undefined;
81-
const to = toStr ? parseInt(toStr, 10) : undefined;
79+
const from = parseFiniteInt(url.searchParams.get("from"));
80+
const to = parseFiniteInt(url.searchParams.get("to"));
8281
const cursor = url.searchParams.get("cursor") ?? undefined;
8382
const directionRaw = url.searchParams.get("direction") ?? undefined;
8483
const direction = directionRaw ? DirectionSchema.parse(directionRaw) : undefined;

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.tasks.scheduled.$taskParam/route.tsx

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ import {
8383
v3SchedulesAddOnPath,
8484
v3TestTaskPath,
8585
} from "~/utils/pathBuilder";
86+
import { parseFiniteInt } from "~/utils/searchParams";
8687

8788
export const meta: MetaFunction<typeof loader> = ({ data }) => {
8889
const slug = (data as { task?: TaskDetail | null } | undefined)?.task?.slug;
@@ -108,10 +109,8 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
108109

109110
const url = new URL(request.url);
110111
const period = url.searchParams.get("period") ?? undefined;
111-
const fromStr = url.searchParams.get("from");
112-
const toStr = url.searchParams.get("to");
113-
const from = fromStr ? parseInt(fromStr, 10) : undefined;
114-
const to = toStr ? parseInt(toStr, 10) : undefined;
112+
const from = parseFiniteInt(url.searchParams.get("from"));
113+
const to = parseFiniteInt(url.searchParams.get("to"));
115114
const cursor = url.searchParams.get("cursor") ?? undefined;
116115
const directionRaw = url.searchParams.get("direction") ?? undefined;
117116
const direction = directionRaw ? DirectionSchema.parse(directionRaw) : undefined;
@@ -144,8 +143,8 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
144143
})
145144
.catch(() => ({ data: [], statuses: [] } satisfies TaskActivity));
146145

147-
const pageRaw = parseInt(url.searchParams.get("page") ?? "1", 10);
148-
const schedulesPage = Number.isFinite(pageRaw) && pageRaw > 0 ? pageRaw : 1;
146+
const pageRaw = parseFiniteInt(url.searchParams.get("page"));
147+
const schedulesPage = pageRaw !== undefined && pageRaw > 0 ? pageRaw : 1;
149148

150149
// Resolved synchronously — the bottom usage bar reads `limits` and
151150
// `canPurchaseSchedules` directly from it, and the limit-exceeded

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.tasks.standard.$taskParam/route.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import {
4747
v3QueuesPath,
4848
v3TestTaskPath,
4949
} from "~/utils/pathBuilder";
50+
import { parseFiniteInt } from "~/utils/searchParams";
5051

5152
export const meta: MetaFunction<typeof loader> = ({ data }) => {
5253
const slug = (data as { task?: TaskDetail | null } | undefined)?.task?.slug;
@@ -70,10 +71,8 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
7071

7172
const url = new URL(request.url);
7273
const period = url.searchParams.get("period") ?? undefined;
73-
const fromStr = url.searchParams.get("from");
74-
const toStr = url.searchParams.get("to");
75-
const from = fromStr ? parseInt(fromStr, 10) : undefined;
76-
const to = toStr ? parseInt(toStr, 10) : undefined;
74+
const from = parseFiniteInt(url.searchParams.get("from"));
75+
const to = parseFiniteInt(url.searchParams.get("to"));
7776
const cursor = url.searchParams.get("cursor") ?? undefined;
7877
const directionRaw = url.searchParams.get("direction") ?? undefined;
7978
const direction = directionRaw ? DirectionSchema.parse(directionRaw) : undefined;

apps/webapp/app/utils/searchParams.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,18 @@ export const runIdsQueryParam = z
1818
return [...new Set(ids)].slice(0, 100);
1919
});
2020

21+
/**
22+
* `parseInt` accepts garbage-suffixed numbers (`parseInt("123abc", 10) === 123`)
23+
* and returns `NaN` for non-numeric input. Use this helper at loader boundaries
24+
* for URL-supplied integer params so a malformed URL silently falls back to
25+
* `undefined` rather than nudging downstream logic with a partial or NaN value.
26+
*/
27+
export function parseFiniteInt(value: string | null | undefined): number | undefined {
28+
if (value == null || value === "") return undefined;
29+
const n = Number.parseInt(value, 10);
30+
return Number.isFinite(n) ? n : undefined;
31+
}
32+
2133
export function objectToSearchParams(
2234
obj:
2335
| undefined

0 commit comments

Comments
 (0)