Skip to content

Commit cb6477b

Browse files
committed
more fixes
1 parent 768dab4 commit cb6477b

11 files changed

Lines changed: 857 additions & 121 deletions

File tree

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ To see all development ports, refer to the index.html of `apps/dev-launchpad/pub
9595
- When building internal tools for Stack Auth developers (eg. internal interfaces like the WAL info log etc.): Make the interfaces look very concise, assume the user is a pro-user. This only applies to internal tools that are used primarily by Stack Auth developers.
9696
- When building frontend or React code for the dashboard, refer to DESIGN-GUIDE.md.
9797
- NEVER implement a hacky solution without EXPLICIT approval from the user. Always go the extra mile to make sure the solution is clean, maintainable, and robust.
98+
- Fail early, fail loud. Fail fast with an error instead of silently continuing.
9899

99100
### Code-related
100101
- Use ES6 maps instead of records wherever you can.

apps/backend/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@
9494
"pg": "^8.16.3",
9595
"postgres": "^3.4.5",
9696
"posthog-node": "^4.1.0",
97+
"re2": "^1.23.1",
9798
"react": "19.2.3",
9899
"react-dom": "19.2.3",
99100
"resend": "^6.0.1",
@@ -113,6 +114,7 @@
113114
"@types/nodemailer": "^6.4.14",
114115
"@types/oidc-provider": "^8.5.1",
115116
"@types/pg": "^8.16.0",
117+
"@types/re2": "^1.10.8",
116118
"@types/react": "19.2.7",
117119
"@types/react-dom": "19.2.3",
118120
"@types/semver": "^7.5.8",

apps/backend/src/lib/cel-evaluator.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { evaluate, parse, CelEvaluationError, CelParseError, CelTypeError } from "cel-js";
1+
import { CelEvaluationError, CelParseError, CelTypeError, evaluate, parse } from "cel-js";
2+
import RE2 from "re2";
23

34
/**
45
* Context variables available for sign-up rule CEL expressions.
@@ -75,8 +76,12 @@ function preprocessExpression(
7576
}
7677
case 'matches': {
7778
try {
78-
result = new RegExp(arg).test(varValue);
79+
// Use RE2 for regex matching to prevent ReDoS attacks
80+
// RE2 uses a linear-time matching algorithm, preventing catastrophic backtracking
81+
const regex = new RE2(arg);
82+
result = regex.test(varValue);
7983
} catch {
84+
// Invalid regex pattern - treat as non-match
8085
result = false;
8186
}
8287
break;

apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/sign-up-rules/page-client.tsx

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,26 @@
22

33
import { ConditionBuilder } from "@/components/rule-builder";
44
import {
5-
ActionDialog,
6-
Alert,
7-
Button,
8-
cn,
9-
Input,
10-
Select,
11-
SelectContent,
12-
SelectItem,
13-
SelectTrigger,
14-
SelectValue,
15-
Spinner,
16-
Switch,
17-
Typography,
5+
ActionDialog,
6+
Alert,
7+
Button,
8+
cn,
9+
Input,
10+
Select,
11+
SelectContent,
12+
SelectItem,
13+
SelectTrigger,
14+
SelectValue,
15+
Spinner,
16+
Switch,
17+
Typography,
1818
} from "@/components/ui";
1919
import {
20-
createEmptyCondition,
21-
createEmptyGroup,
22-
parseCelToVisualTree,
23-
visualTreeToCel,
24-
type RuleNode,
20+
createEmptyCondition,
21+
createEmptyGroup,
22+
parseCelToVisualTree,
23+
visualTreeToCel,
24+
type RuleNode,
2525
} from "@/lib/cel-visual-parser";
2626
import { useUpdateConfig } from "@/lib/config-update";
2727
import { closestCenter, DndContext, KeyboardSensor, PointerSensor, useSensor, useSensors, type DragEndEvent } from '@dnd-kit/core';
Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import PageClient from "./page-client";
1+
"use client";
22

3-
export default async function Page() {
4-
return <PageClient />;
5-
}
3+
export { default } from "./page-client";

apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/users/[userId]/page-client.tsx

Lines changed: 36 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -6,42 +6,42 @@ import { InputField, SelectField } from "@/components/form-fields";
66
import { StyledLink } from "@/components/link";
77
import { SettingCard } from "@/components/settings";
88
import {
9-
Accordion,
10-
AccordionContent,
11-
AccordionItem,
12-
AccordionTrigger,
13-
ActionCell,
14-
Alert,
15-
AlertDescription,
16-
AlertTitle,
17-
Avatar,
18-
AvatarFallback,
19-
AvatarImage,
20-
Button,
21-
Dialog,
22-
DialogContent,
23-
DialogDescription,
24-
DialogFooter,
25-
DialogHeader,
26-
DialogTitle,
27-
DropdownMenu,
28-
DropdownMenuContent,
29-
DropdownMenuItem,
30-
DropdownMenuSeparator,
31-
DropdownMenuTrigger,
32-
Input,
33-
Separator,
34-
SimpleTooltip,
35-
Table,
36-
TableBody,
37-
TableCell,
38-
TableHead,
39-
TableHeader,
40-
TableRow,
41-
Textarea,
42-
Typography,
43-
cn,
44-
useToast
9+
Accordion,
10+
AccordionContent,
11+
AccordionItem,
12+
AccordionTrigger,
13+
ActionCell,
14+
Alert,
15+
AlertDescription,
16+
AlertTitle,
17+
Avatar,
18+
AvatarFallback,
19+
AvatarImage,
20+
Button,
21+
Dialog,
22+
DialogContent,
23+
DialogDescription,
24+
DialogFooter,
25+
DialogHeader,
26+
DialogTitle,
27+
DropdownMenu,
28+
DropdownMenuContent,
29+
DropdownMenuItem,
30+
DropdownMenuSeparator,
31+
DropdownMenuTrigger,
32+
Input,
33+
Separator,
34+
SimpleTooltip,
35+
Table,
36+
TableBody,
37+
TableCell,
38+
TableHead,
39+
TableHeader,
40+
TableRow,
41+
Textarea,
42+
Typography,
43+
cn,
44+
useToast
4545
} from "@/components/ui";
4646
import { DeleteUserDialog, ImpersonateUserDialog } from "@/components/user-dialogs";
4747
import { useThemeWatcher } from '@/lib/theme';

apps/dashboard/src/components/rule-builder/condition-builder.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@
22

33
import { Button, cn } from "@/components/ui";
44
import {
5-
type ConditionField,
6-
type ConditionNode,
7-
type ConditionOperator,
8-
createEmptyCondition,
9-
createEmptyGroup,
10-
type GroupNode,
11-
type RuleNode,
5+
type ConditionField,
6+
type ConditionNode,
7+
type ConditionOperator,
8+
createEmptyCondition,
9+
createEmptyGroup,
10+
type GroupNode,
11+
type RuleNode,
1212
} from "@/lib/cel-visual-parser";
1313
import { PlusIcon, TrashIcon } from "@phosphor-icons/react";
1414
import React from "react";
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { visualTreeToCel, parseCelToVisualTree, createEmptyCondition } from './cel-visual-parser';
3+
4+
describe('cel-visual-parser', () => {
5+
describe('CEL string escaping', () => {
6+
it('should escape double quotes in condition values', () => {
7+
const condition = {
8+
...createEmptyCondition(),
9+
field: 'email' as const,
10+
operator: 'contains' as const,
11+
value: 'test"value',
12+
};
13+
14+
const cel = visualTreeToCel(condition);
15+
// Should escape the quote
16+
expect(cel).toBe('email.contains("test\\"value")');
17+
// Should NOT contain unescaped quote that would break CEL
18+
expect(cel).not.toMatch(/contains\("test"value"\)/);
19+
});
20+
21+
it('should escape backslashes in condition values', () => {
22+
const condition = {
23+
...createEmptyCondition(),
24+
field: 'email' as const,
25+
operator: 'contains' as const,
26+
value: 'test\\value',
27+
};
28+
29+
const cel = visualTreeToCel(condition);
30+
// Should escape the backslash
31+
expect(cel).toBe('email.contains("test\\\\value")');
32+
});
33+
34+
it('should escape both quotes and backslashes together', () => {
35+
const condition = {
36+
...createEmptyCondition(),
37+
field: 'email' as const,
38+
operator: 'equals' as const,
39+
value: 'test\\"value',
40+
};
41+
42+
const cel = visualTreeToCel(condition);
43+
// Backslash escaped first, then quote
44+
expect(cel).toBe('email == "test\\\\\\"value"');
45+
});
46+
47+
it('should prevent CEL injection via malicious value', () => {
48+
// Attacker tries: test" || true || "
49+
// Without escaping this becomes: email == "test" || true || ""
50+
// Which would always be true due to || true
51+
const condition = {
52+
...createEmptyCondition(),
53+
field: 'email' as const,
54+
operator: 'equals' as const,
55+
value: 'test" || true || "',
56+
};
57+
58+
const cel = visualTreeToCel(condition);
59+
// Should escape quotes, preventing injection
60+
expect(cel).toBe('email == "test\\" || true || \\""');
61+
// Should NOT allow the injection pattern
62+
expect(cel).not.toContain('" || true || "');
63+
});
64+
65+
it('should escape values in all operator types', () => {
66+
const maliciousValue = 'inject"attack';
67+
68+
// Test equals
69+
expect(visualTreeToCel({
70+
...createEmptyCondition(),
71+
field: 'email' as const,
72+
operator: 'equals' as const,
73+
value: maliciousValue,
74+
})).toContain('\\"');
75+
76+
// Test not_equals
77+
expect(visualTreeToCel({
78+
...createEmptyCondition(),
79+
field: 'email' as const,
80+
operator: 'not_equals' as const,
81+
value: maliciousValue,
82+
})).toContain('\\"');
83+
84+
// Test matches
85+
expect(visualTreeToCel({
86+
...createEmptyCondition(),
87+
field: 'email' as const,
88+
operator: 'matches' as const,
89+
value: maliciousValue,
90+
})).toContain('\\"');
91+
92+
// Test ends_with
93+
expect(visualTreeToCel({
94+
...createEmptyCondition(),
95+
field: 'email' as const,
96+
operator: 'ends_with' as const,
97+
value: maliciousValue,
98+
})).toContain('\\"');
99+
100+
// Test starts_with
101+
expect(visualTreeToCel({
102+
...createEmptyCondition(),
103+
field: 'email' as const,
104+
operator: 'starts_with' as const,
105+
value: maliciousValue,
106+
})).toContain('\\"');
107+
108+
// Test contains
109+
expect(visualTreeToCel({
110+
...createEmptyCondition(),
111+
field: 'email' as const,
112+
operator: 'contains' as const,
113+
value: maliciousValue,
114+
})).toContain('\\"');
115+
});
116+
117+
it('should escape values in in_list operator', () => {
118+
const condition = {
119+
...createEmptyCondition(),
120+
field: 'emailDomain' as const,
121+
operator: 'in_list' as const,
122+
value: ['safe.com', 'inject"attack.com', 'also\\bad.com'],
123+
};
124+
125+
const cel = visualTreeToCel(condition);
126+
expect(cel).toContain('inject\\"attack.com');
127+
expect(cel).toContain('also\\\\bad.com');
128+
});
129+
});
130+
131+
describe('CEL to visual tree parsing', () => {
132+
it('should parse simple equality condition', () => {
133+
const result = parseCelToVisualTree('email == "test@example.com"');
134+
expect(result).toBeDefined();
135+
if (result?.type === 'condition') {
136+
expect(result.field).toBe('email');
137+
expect(result.operator).toBe('equals');
138+
expect(result.value).toBe('test@example.com');
139+
}
140+
});
141+
142+
it('should parse endsWith condition', () => {
143+
const result = parseCelToVisualTree('email.endsWith("@gmail.com")');
144+
expect(result).toBeDefined();
145+
if (result?.type === 'condition') {
146+
expect(result.field).toBe('email');
147+
expect(result.operator).toBe('ends_with');
148+
expect(result.value).toBe('@gmail.com');
149+
}
150+
});
151+
});
152+
});

0 commit comments

Comments
 (0)