a reactive (signals based) hypermedia web framework (wip)
stormlightlabs.github.io/volt/
hypermedia
frontend
signals
1import { evaluate, evaluateStatements } from "$core/evaluator";
2import type { Scope } from "$types/volt";
3import { describe, expect, it } from "vitest";
4
5describe("Evaluator - Security Tests", () => {
6 describe("Prototype Pollution Protection", () => {
7 it("should block __proto__ access", () => {
8 const scope: Scope = {};
9 const result = evaluate("__proto__", scope);
10 expect(result).toBe(undefined);
11 });
12
13 it("should block __proto__ assignment", () => {
14 const scope: Scope = {};
15 evaluateStatements("__proto__ = {}", scope);
16 expect(Object.hasOwn(scope, "__proto__")).toBe(false);
17 });
18
19 it("should block constructor access", () => {
20 const scope: Scope = { obj: {} };
21 expect(evaluate("obj.constructor", scope)).toBe(undefined);
22 });
23
24 it("should block constructor.prototype access", () => {
25 const scope: Scope = { obj: {} };
26 expect(() => evaluate("obj.constructor.prototype", scope)).toThrow();
27 });
28
29 it("should block prototype property access", () => {
30 const scope: Scope = { fn: () => {} };
31 expect(evaluate("fn.prototype", scope)).toBe(undefined);
32 });
33 });
34
35 describe("Dangerous Global Blocking", () => {
36 it("should block Function constructor access", () => {
37 const scope: Scope = {};
38 const result = evaluate("Function", scope);
39 expect(result).toBe(undefined);
40 });
41
42 it("should block eval access", () => {
43 const scope: Scope = {};
44 const result = evaluate("eval", scope);
45 expect(result).toBe(undefined);
46 });
47
48 it("should block globalThis access", () => {
49 const scope: Scope = {};
50 const result = evaluate("globalThis", scope);
51 expect(result).toBe(undefined);
52 });
53
54 it("should block window access", () => {
55 const scope: Scope = {};
56 expect(evaluate("window", scope)).toBe(undefined);
57 });
58
59 it("should block self access", () => {
60 const scope: Scope = {};
61 expect(evaluate("self", scope)).toBe(undefined);
62 });
63
64 it("should block import access", () => {
65 const scope: Scope = {};
66 expect(() => evaluate("import", scope)).toThrow();
67 });
68 });
69
70 describe("Constructor Escape Protection", () => {
71 it("should prevent constructor escape via array", () => {
72 const scope: Scope = { arr: [] };
73 expect(evaluate("arr.constructor", scope)).toBe(undefined);
74 });
75
76 it("should prevent constructor escape via object", () => {
77 const scope: Scope = { obj: {} };
78 expect(evaluate("obj.constructor", scope)).toBe(undefined);
79 });
80
81 it("should prevent constructor escape via function", () => {
82 const scope: Scope = { fn: () => {} };
83 expect(evaluate("fn.constructor", scope)).toBe(undefined);
84 });
85
86 it("should prevent prototype chain traversal", () => {
87 const scope: Scope = { obj: {} };
88 expect(evaluate("obj.__proto__", scope)).toBe(undefined);
89 });
90 });
91
92 describe("Safe Global Access", () => {
93 it("should allow Math access", () => {
94 const scope: Scope = {};
95 expect(evaluate("Math.PI", scope)).toBe(Math.PI);
96 expect(evaluate("Math.max(10, 20)", scope)).toBe(20);
97 });
98
99 it("should allow Date access", () => {
100 const scope: Scope = {};
101 const result = evaluate("Date.now()", scope);
102 expect(typeof result).toBe("number");
103 });
104
105 it("should allow String access", () => {
106 const scope: Scope = {};
107 expect(evaluate("String(123)", scope)).toBe("123");
108 });
109
110 it("should allow Number access", () => {
111 const scope: Scope = {};
112 expect(evaluate("Number('42')", scope)).toBe(42);
113 });
114
115 it("should allow Boolean access", () => {
116 const scope: Scope = {};
117 expect(evaluate("Boolean(1)", scope)).toBe(true);
118 });
119
120 it("should allow Array access", () => {
121 const scope: Scope = {};
122 const result = evaluate("Array.isArray([])", scope);
123 expect(result).toBe(true);
124 });
125
126 it("should allow Object access for safe methods", () => {
127 const scope: Scope = {};
128 const result = evaluate("Object.keys({ a: 1, b: 2 })", scope);
129 expect(result).toEqual(["a", "b"]);
130 });
131
132 it("should allow JSON access", () => {
133 const scope: Scope = {};
134 const result = evaluate("JSON.parse('{\"key\":\"value\"}')", scope);
135 expect(result).toEqual({ key: "value" });
136 });
137
138 it("should allow console access", () => {
139 const scope: Scope = {};
140 expect(() => evaluate("console", scope)).not.toThrow();
141 });
142 });
143
144 describe("Scope Isolation", () => {
145 it("should not leak variables between scopes", () => {
146 const scope1: Scope = { secret: "value1" };
147 const scope2: Scope = { secret: "value2" };
148
149 expect(evaluate("secret", scope1)).toBe("value1");
150 expect(evaluate("secret", scope2)).toBe("value2");
151 });
152
153 it("should return undefined for missing variables", () => {
154 const scope: Scope = {};
155 expect(evaluate("missing", scope)).toBe(undefined);
156 });
157
158 it("should not allow access to scope object internals", () => {
159 const scope: Scope = { value: 42 };
160 const result = evaluate("constructor", scope);
161 expect(result).toBe(undefined);
162 });
163 });
164
165 describe("Safe Property Names", () => {
166 it("should allow normal property names", () => {
167 const scope: Scope = { user: { name: "Alice", age: 30 } };
168 expect(evaluate("user.name", scope)).toBe("Alice");
169 expect(evaluate("user.age", scope)).toBe(30);
170 });
171
172 it("should allow underscore-prefixed properties", () => {
173 const scope: Scope = { _private: "value" };
174 expect(evaluate("_private", scope)).toBe("value");
175 });
176
177 it("should allow dollar-prefixed properties", () => {
178 const scope: Scope = { $special: "value" };
179 expect(evaluate("$special", scope)).toBe("value");
180 });
181
182 it("should allow numeric property access", () => {
183 const scope: Scope = { items: ["a", "b", "c"] };
184 expect(evaluate("items[0]", scope)).toBe("a");
185 expect(evaluate("items[1]", scope)).toBe("b");
186 });
187 });
188
189 describe("Attack Vector Prevention", () => {
190 it("should prevent prototype pollution via __proto__", () => {
191 const scope: Scope = { obj: {} };
192 expect(() => evaluateStatements("obj.__proto__.polluted = true", scope)).toThrow();
193 expect((Object.prototype as Record<string, unknown>).polluted).toBe(undefined);
194 });
195
196 it("should prevent prototype pollution via constructor.prototype", () => {
197 const scope: Scope = { obj: {} };
198 expect(() => evaluateStatements("obj.constructor.prototype.polluted = true", scope)).toThrow();
199 expect((Object.prototype as Record<string, unknown>).polluted).toBe(undefined);
200 });
201
202 it("should prevent code injection via Function constructor", () => {
203 const scope: Scope = { code: "alert('xss')" };
204 expect(() => evaluate("Function(code)", scope)).toThrow(/not a function/);
205 });
206
207 it("should prevent code injection via eval", () => {
208 const scope: Scope = { code: "console.log('test')" };
209 expect(() => evaluate("eval(code)", scope)).toThrow(/not a function/);
210 });
211
212 it("should prevent indirect eval via globalThis", () => {
213 const scope: Scope = { code: "console.log('test')" };
214 expect(() => evaluate("globalThis.eval(code)", scope)).toThrow();
215 });
216 });
217
218 describe("Edge Cases", () => {
219 it("should handle null values safely", () => {
220 const scope: Scope = { value: null };
221 expect(evaluate("value", scope)).toBe(null);
222 });
223
224 it("should handle undefined values safely", () => {
225 const scope: Scope = { value: undefined };
226 expect(evaluate("value", scope)).toBe(undefined);
227 });
228
229 it("should handle empty strings safely", () => {
230 const scope: Scope = { value: "" };
231 expect(evaluate("value", scope)).toBe("");
232 });
233
234 it("should handle symbol keys safely", () => {
235 const scope: Scope = {};
236 const sym = Symbol("test");
237 scope[sym as unknown as string] = "value";
238 expect(evaluate("test", scope)).toBe(undefined);
239 });
240 });
241
242 describe("Object.create(null) Protection", () => {
243 it("should work with null-prototype objects", () => {
244 const scope: Scope = { nullProto: Object.create(null) };
245 // @ts-expect-error cast from unknown
246 scope.nullProto.value = 42;
247 expect(evaluate("nullProto.value", scope)).toBe(42);
248 });
249
250 it("should prevent attacks on null-prototype objects", () => {
251 const scope: Scope = { nullProto: Object.create(null) };
252 // constructor property is blocked, returns undefined
253 expect(evaluate("nullProto.constructor", scope)).toBe(undefined);
254 });
255 });
256});