Skip to content

Commit 09afbeb

Browse files
feat: add tests for disallowed identifier detection in object coercion scenarios (#55)
* feat: add tests for disallowed identifier detection in object coercion scenarios (cherry picked from commit 759061e) * feat: enhance coercion handling in object expressions to resolve toString and valueOf methods * test: refactor enclave tests to ensure proper resource disposal with try-finally blocks
1 parent 1dd877c commit 09afbeb

5 files changed

Lines changed: 1315 additions & 415 deletions

File tree

libs/ast/src/__tests__/rules.spec.ts

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,214 @@ describe('Validation Rules', () => {
3535
expect(result.valid).toBe(true);
3636
});
3737

38+
it('should detect object with toString arrow returning disallowed identifier', async () => {
39+
const rule = new DisallowedIdentifierRule({ disallowed: ['constructor'] });
40+
const validator = new JSAstValidator([rule]);
41+
42+
const result = await validator.validate('obj[{toString: () => "constructor"}]', {
43+
rules: { 'disallowed-identifier': true },
44+
});
45+
expect(result.valid).toBe(false);
46+
expect(result.issues[0].code).toBe('DISALLOWED_IDENTIFIER');
47+
expect(result.issues[0].data?.['identifier']).toBe('constructor');
48+
});
49+
50+
it('should detect object with toString method shorthand returning disallowed identifier', async () => {
51+
const rule = new DisallowedIdentifierRule({ disallowed: ['constructor'] });
52+
const validator = new JSAstValidator([rule]);
53+
54+
const result = await validator.validate('obj[{toString() { return "constructor" }}]', {
55+
rules: { 'disallowed-identifier': true },
56+
});
57+
expect(result.valid).toBe(false);
58+
expect(result.issues[0].code).toBe('DISALLOWED_IDENTIFIER');
59+
expect(result.issues[0].data?.['identifier']).toBe('constructor');
60+
});
61+
62+
it('should detect object with valueOf arrow returning disallowed identifier', async () => {
63+
const rule = new DisallowedIdentifierRule({ disallowed: ['__proto__'] });
64+
const validator = new JSAstValidator([rule]);
65+
66+
const result = await validator.validate('obj[{valueOf: () => "__proto__"}]', {
67+
rules: { 'disallowed-identifier': true },
68+
});
69+
expect(result.valid).toBe(false);
70+
expect(result.issues[0].code).toBe('DISALLOWED_IDENTIFIER');
71+
expect(result.issues[0].data?.['identifier']).toBe('__proto__');
72+
});
73+
74+
it('should detect object with toString function expression returning disallowed identifier', async () => {
75+
const rule = new DisallowedIdentifierRule({ disallowed: ['prototype'] });
76+
const validator = new JSAstValidator([rule]);
77+
78+
const result = await validator.validate('obj[{toString: function() { return "prototype" }}]', {
79+
rules: { 'disallowed-identifier': true },
80+
});
81+
expect(result.valid).toBe(false);
82+
expect(result.issues[0].code).toBe('DISALLOWED_IDENTIFIER');
83+
expect(result.issues[0].data?.['identifier']).toBe('prototype');
84+
});
85+
86+
it('should detect object inside array coercion', async () => {
87+
const rule = new DisallowedIdentifierRule({ disallowed: ['constructor'] });
88+
const validator = new JSAstValidator([rule]);
89+
90+
const result = await validator.validate('obj[[{toString: () => "constructor"}]]', {
91+
rules: { 'disallowed-identifier': true },
92+
});
93+
expect(result.valid).toBe(false);
94+
expect(result.issues[0].code).toBe('DISALLOWED_IDENTIFIER');
95+
expect(result.issues[0].data?.['identifier']).toBe('constructor');
96+
});
97+
98+
it('should not false positive on safe objects without toString/valueOf', async () => {
99+
const rule = new DisallowedIdentifierRule({ disallowed: ['constructor', '__proto__'] });
100+
const validator = new JSAstValidator([rule]);
101+
102+
const result = await validator.validate('obj[{foo: "bar"}]', {
103+
rules: { 'disallowed-identifier': true },
104+
});
105+
expect(result.valid).toBe(true);
106+
});
107+
108+
it('should not false positive on toString returning non-disallowed string', async () => {
109+
const rule = new DisallowedIdentifierRule({ disallowed: ['constructor', '__proto__'] });
110+
const validator = new JSAstValidator([rule]);
111+
112+
const result = await validator.validate('obj[{toString: () => "safe"}]', {
113+
rules: { 'disallowed-identifier': true },
114+
});
115+
expect(result.valid).toBe(true);
116+
});
117+
118+
it('should detect template literal key', async () => {
119+
const rule = new DisallowedIdentifierRule({ disallowed: ['constructor'] });
120+
const validator = new JSAstValidator([rule]);
121+
122+
const result = await validator.validate('obj[`constructor`]', {
123+
rules: { 'disallowed-identifier': true },
124+
});
125+
expect(result.valid).toBe(false);
126+
expect(result.issues[0].code).toBe('DISALLOWED_IDENTIFIER');
127+
expect(result.issues[0].data?.['identifier']).toBe('constructor');
128+
});
129+
130+
it('should detect conditional expression (consequent)', async () => {
131+
const rule = new DisallowedIdentifierRule({ disallowed: ['constructor'] });
132+
const validator = new JSAstValidator([rule]);
133+
134+
const result = await validator.validate("obj[true ? 'constructor' : 'x']", {
135+
rules: { 'disallowed-identifier': true },
136+
});
137+
expect(result.valid).toBe(false);
138+
expect(result.issues[0].code).toBe('DISALLOWED_IDENTIFIER');
139+
expect(result.issues[0].data?.['identifier']).toBe('constructor');
140+
});
141+
142+
it('should detect conditional expression (alternate)', async () => {
143+
const rule = new DisallowedIdentifierRule({ disallowed: ['constructor'] });
144+
const validator = new JSAstValidator([rule]);
145+
146+
const result = await validator.validate("obj[false ? 'x' : 'constructor']", {
147+
rules: { 'disallowed-identifier': true },
148+
});
149+
expect(result.valid).toBe(false);
150+
expect(result.issues[0].code).toBe('DISALLOWED_IDENTIFIER');
151+
expect(result.issues[0].data?.['identifier']).toBe('constructor');
152+
});
153+
154+
it('should detect sequence expression', async () => {
155+
const rule = new DisallowedIdentifierRule({ disallowed: ['constructor'] });
156+
const validator = new JSAstValidator([rule]);
157+
158+
const result = await validator.validate("obj[(0, 'constructor')]", {
159+
rules: { 'disallowed-identifier': true },
160+
});
161+
expect(result.valid).toBe(false);
162+
expect(result.issues[0].code).toBe('DISALLOWED_IDENTIFIER');
163+
expect(result.issues[0].data?.['identifier']).toBe('constructor');
164+
});
165+
166+
it('should detect assignment expression as computed key', async () => {
167+
const rule = new DisallowedIdentifierRule({ disallowed: ['constructor'] });
168+
const validator = new JSAstValidator([rule]);
169+
170+
const result = await validator.validate("let x; obj[x = 'constructor']", {
171+
rules: { 'disallowed-identifier': true },
172+
});
173+
expect(result.valid).toBe(false);
174+
expect(result.issues[0].code).toBe('DISALLOWED_IDENTIFIER');
175+
expect(result.issues[0].data?.['identifier']).toBe('constructor');
176+
});
177+
178+
it('should detect logical OR expression', async () => {
179+
const rule = new DisallowedIdentifierRule({ disallowed: ['constructor'] });
180+
const validator = new JSAstValidator([rule]);
181+
182+
const result = await validator.validate("obj['' || 'constructor']", {
183+
rules: { 'disallowed-identifier': true },
184+
});
185+
expect(result.valid).toBe(false);
186+
expect(result.issues[0].code).toBe('DISALLOWED_IDENTIFIER');
187+
expect(result.issues[0].data?.['identifier']).toBe('constructor');
188+
});
189+
190+
it('should detect logical AND expression', async () => {
191+
const rule = new DisallowedIdentifierRule({ disallowed: ['constructor'] });
192+
const validator = new JSAstValidator([rule]);
193+
194+
const result = await validator.validate("obj['constructor' && 'constructor']", {
195+
rules: { 'disallowed-identifier': true },
196+
});
197+
expect(result.valid).toBe(false);
198+
expect(result.issues[0].code).toBe('DISALLOWED_IDENTIFIER');
199+
expect(result.issues[0].data?.['identifier']).toBe('constructor');
200+
});
201+
202+
it('should detect nullish coalescing expression', async () => {
203+
const rule = new DisallowedIdentifierRule({ disallowed: ['constructor'] });
204+
const validator = new JSAstValidator([rule]);
205+
206+
const result = await validator.validate("obj[null ?? 'constructor']", {
207+
rules: { 'disallowed-identifier': true },
208+
});
209+
expect(result.valid).toBe(false);
210+
expect(result.issues[0].code).toBe('DISALLOWED_IDENTIFIER');
211+
expect(result.issues[0].data?.['identifier']).toBe('constructor');
212+
});
213+
214+
it('should detect getter-based toString coercion', async () => {
215+
const rule = new DisallowedIdentifierRule({ disallowed: ['constructor'] });
216+
const validator = new JSAstValidator([rule]);
217+
218+
const result = await validator.validate("obj[{get toString(){ return () => 'constructor' }}]", {
219+
rules: { 'disallowed-identifier': true },
220+
});
221+
expect(result.valid).toBe(false);
222+
expect(result.issues[0].code).toBe('DISALLOWED_IDENTIFIER');
223+
expect(result.issues[0].data?.['identifier']).toBe('constructor');
224+
});
225+
226+
it('should allow template literal with expressions (not statically resolvable)', async () => {
227+
const rule = new DisallowedIdentifierRule({ disallowed: ['constructor'] });
228+
const validator = new JSAstValidator([rule]);
229+
230+
const result = await validator.validate("obj[`${'con'}structor`]", {
231+
rules: { 'disallowed-identifier': true },
232+
});
233+
expect(result.valid).toBe(true);
234+
});
235+
236+
it('should allow safe template literal', async () => {
237+
const rule = new DisallowedIdentifierRule({ disallowed: ['constructor'] });
238+
const validator = new JSAstValidator([rule]);
239+
240+
const result = await validator.validate('obj[`safe`]', {
241+
rules: { 'disallowed-identifier': true },
242+
});
243+
expect(result.valid).toBe(true);
244+
});
245+
38246
it('should use custom message template', async () => {
39247
const rule = new DisallowedIdentifierRule({
40248
disallowed: ['eval'],

0 commit comments

Comments
 (0)