diff --git a/server/routers/badger/verifySession.ts b/server/routers/badger/verifySession.ts index 12058825..39415023 100644 --- a/server/routers/badger/verifySession.ts +++ b/server/routers/badger/verifySession.ts @@ -480,7 +480,7 @@ async function checkRules( return rule.action == "ACCEPT"; } else if (path && rule.match == "PATH") { // rule.value is a regex, match on the path and see if it matches - const re = new RegExp(rule.value); + const re = urlGlobToRegex(rule.value); if (re.test(path)) { return rule.action == "ACCEPT"; } @@ -489,3 +489,19 @@ async function checkRules( return false; } + +function urlGlobToRegex(pattern: string): RegExp { + // Remove leading slash if present (we'll add it to the regex pattern) + pattern = pattern.startsWith('/') ? pattern.slice(1) : pattern; + + // Escape special regex characters except * + const escapedPattern = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&'); + + // Replace * with regex pattern for any valid URL segment characters + const regexPattern = escapedPattern.replace(/\*/g, '[a-zA-Z0-9_-]+'); + + // Create the final pattern that: + // 1. Optionally matches leading slash + // 2. Matches the entire string + return new RegExp(`^/?${regexPattern}$`); + } \ No newline at end of file diff --git a/src/app/[orgId]/settings/resources/[resourceId]/rules/page.tsx b/src/app/[orgId]/settings/resources/[resourceId]/rules/page.tsx index 2398bf32..d74ba6e9 100644 --- a/src/app/[orgId]/settings/resources/[resourceId]/rules/page.tsx +++ b/src/app/[orgId]/settings/resources/[resourceId]/rules/page.tsx @@ -93,9 +93,9 @@ export default function ResourceRules(props: { useEffect(() => { const fetchRules = async () => { try { - const res = await api.get>( - `/resource/${params.resourceId}/rules` - ); + const res = await api.get< + AxiosResponse + >(`/resource/${params.resourceId}/rules`); if (res.status === 200) { setRules(res.data.data.rules); } @@ -133,6 +133,25 @@ export default function ResourceRules(props: { return; } + if (data.match === "CIDR" && !isValidCIDR(data.value)) { + toast({ + variant: "destructive", + title: "Invalid CIDR", + description: "Please enter a valid CIDR value" + }); + setLoading(false); + return; + } + if (data.match === "PATH" && !isValidUrlGlobPattern(data.value)) { + toast({ + variant: "destructive", + title: "Invalid URL path", + description: "Please enter a valid URL path value" + }); + setLoading(false); + return; + } + const newRule: LocalRule = { ...data, ruleId: new Date().getTime(), @@ -171,11 +190,27 @@ export default function ResourceRules(props: { value: rule.value }; + if (rule.match === "CIDR" && !isValidCIDR(rule.value)) { + toast({ + variant: "destructive", + title: "Invalid CIDR", + description: "Please enter a valid CIDR value" + }); + setLoading(false); + return; + } + if (rule.match === "PATH" && !isValidUrlGlobPattern(rule.value)) { + toast({ + variant: "destructive", + title: "Invalid URL path", + description: "Please enter a valid URL path value" + }); + setLoading(false); + return; + } + if (rule.new) { - await api.put( - `/resource/${params.resourceId}/rule`, - data - ); + await api.put(`/resource/${params.resourceId}/rule`, data); } else if (rule.updated) { await api.post( `/resource/${params.resourceId}/rule/${rule.ruleId}`, @@ -190,7 +225,9 @@ export default function ResourceRules(props: { ); } - setRules(rules.map(rule => ({ ...rule, new: false, updated: false }))); + setRules( + rules.map((rule) => ({ ...rule, new: false, updated: false })) + ); setRulesToRemove([]); toast({ @@ -322,7 +359,9 @@ export default function ResourceRules(props: { @@ -380,7 +421,8 @@ export default function ResourceRules(props: { - Enter CIDR or path value based on match type + Enter CIDR or path value based + on match type )} @@ -401,7 +443,8 @@ export default function ResourceRules(props: { {header.isPlaceholder ? null : flexRender( - header.column.columnDef.header, + header.column + .columnDef.header, header.getContext() )} @@ -413,14 +456,17 @@ export default function ResourceRules(props: { {table.getRowModel().rows?.length ? ( table.getRowModel().rows.map((row) => ( - {row.getVisibleCells().map((cell) => ( - - {flexRender( - cell.column.columnDef.cell, - cell.getContext() - )} - - ))} + {row + .getVisibleCells() + .map((cell) => ( + + {flexRender( + cell.column + .columnDef.cell, + cell.getContext() + )} + + ))} )) ) : ( @@ -449,4 +495,58 @@ export default function ResourceRules(props: { ); -} \ No newline at end of file +} + +function isValidCIDR(cidr: string): boolean { + // Match CIDR pattern (e.g., "192.168.0.0/24") + const cidrPattern = + /^([0-9]{1,3}\.){3}[0-9]{1,3}\/([0-9]|[1-2][0-9]|3[0-2])$/; + + if (!cidrPattern.test(cidr)) { + return false; + } + + // Validate IP address part + const ipPart = cidr.split("/")[0]; + const octets = ipPart.split("."); + + return octets.every((octet) => { + const num = parseInt(octet, 10); + return num >= 0 && num <= 255; + }); +} + +function isValidUrlGlobPattern(pattern: string): boolean { + // Remove leading slash if present + pattern = pattern.startsWith('/') ? pattern.slice(1) : pattern; + + // Empty string is not valid + if (!pattern) { + return false; + } + + // Split path into segments + const segments = pattern.split('/'); + + // Check each segment + for (let i = 0; i < segments.length; i++) { + const segment = segments[i]; + + // Empty segments are not allowed (double slashes) + if (!segment && i !== segments.length - 1) { + return false; + } + + // If segment contains *, it must be exactly * + if (segment.includes('*') && segment !== '*') { + return false; + } + + // Check for invalid characters + if (!/^[a-zA-Z0-9_*-]*$/.test(segment)) { + return false; + } + } + + return true; + } \ No newline at end of file