a reactive (signals based) hypermedia web framework (wip) stormlightlabs.github.io/volt/
hypermedia frontend signals
at main 256 lines 8.7 kB view raw
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});