UNPKG

npm-api-analyzer

Version:

CLI tool to analyze npm packages for network API usage, prototype pollution, and security vulnerabilities

612 lines (494 loc) 23.1 kB
import { APIDetector } from '../src/apiDetector'; import { APIUsage } from '../src/types'; describe('Prototype Pollution Detection', () => { // Helper function to get prototype pollution results const getPrototypePollutionResults = (code: string): APIUsage[] => { const results = APIDetector.detectAPIUsages(code, 'test.js'); return results.filter(r => r.category === 'prototype-pollution'); }; describe('Direct Prototype Modifications - Dot Notation', () => { test('should detect Object.prototype dot assignment', () => { const code = 'Object.prototype.polluted = true;'; const results = getPrototypePollutionResults(code); expect(results).toHaveLength(1); expect(results[0].api).toBe('Object.prototype.property ='); expect(results[0].riskLevel).toBe('high'); expect(results[0].location.line).toBe(1); }); test('should detect Array.prototype dot assignment', () => { const code = 'Array.prototype.customMethod = function() {};'; const results = getPrototypePollutionResults(code); expect(results).toHaveLength(1); expect(results[0].api).toBe('Array.prototype.property ='); expect(results[0].riskLevel).toBe('high'); }); test('should detect all built-in prototype modifications', () => { const code = ` Object.prototype.a = 1; Array.prototype.b = 2; String.prototype.c = 3; Function.prototype.d = 4; Number.prototype.e = 5; Boolean.prototype.f = 6; Date.prototype.g = 7; RegExp.prototype.h = 8; `; const results = getPrototypePollutionResults(code); expect(results).toHaveLength(8); results.forEach(result => { expect(result.riskLevel).toBe('high'); expect(result.api).toMatch(/\.prototype\.property =/); }); }); }); describe('Direct Prototype Modifications - Bracket Notation', () => { test('should detect Object.prototype bracket assignment', () => { const code = 'Object.prototype["polluted"] = true;'; const results = getPrototypePollutionResults(code); expect(results).toHaveLength(1); expect(results[0].api).toBe('Object.prototype[key] ='); expect(results[0].riskLevel).toBe('high'); }); test('should detect dynamic key assignments', () => { const code = ` Object.prototype[userKey] = malicious; Array.prototype[dynamicProp] = evil; `; const results = getPrototypePollutionResults(code); expect(results).toHaveLength(2); results.forEach(result => { expect(result.riskLevel).toBe('high'); }); }); }); describe('__proto__ Manipulations', () => { test('should detect direct __proto__ assignment', () => { const code = 'obj.__proto__ = malicious;'; const results = getPrototypePollutionResults(code); expect(results).toHaveLength(1); expect(results[0].api).toBe('.__proto__ ='); expect(results[0].riskLevel).toBe('high'); }); test('should detect __proto__ bracket assignment', () => { const code = 'obj["__proto__"] = payload;'; const results = getPrototypePollutionResults(code); expect(results).toHaveLength(2); // Both patterns match expect(results.some(r => r.api === '["__proto__"] =')).toBe(true); expect(results.some(r => r.api === 'Dynamic __proto__ access')).toBe(true); }); test('should detect __proto__ property access', () => { const code = 'obj.__proto__["key"] = value;'; const results = getPrototypePollutionResults(code); expect(results).toHaveLength(1); expect(results[0].api).toBe('__proto__[key] ='); expect(results[0].riskLevel).toBe('high'); }); test('should detect nested __proto__ access', () => { const code = 'data.__proto__.__proto__.polluted = true;'; const results = getPrototypePollutionResults(code); expect(results.length).toBeGreaterThan(0); expect(results.some(r => r.api === '.__proto__.property')).toBe(true); }); }); describe('Constructor Prototype Modifications', () => { test('should detect constructor prototype modifications', () => { const code = 'obj.constructor.prototype["polluted"] = true;'; const results = getPrototypePollutionResults(code); expect(results).toHaveLength(1); expect(results[0].api).toBe('.constructor.prototype[key] ='); expect(results[0].riskLevel).toBe('high'); }); test('should detect nested constructor prototype access', () => { const code = 'data.constructor.prototype[userInput] = payload;'; const results = getPrototypePollutionResults(code); expect(results).toHaveLength(1); expect(results[0].riskLevel).toBe('high'); }); }); describe('Object Methods for Prototype Manipulation', () => { test('should detect Object.setPrototypeOf', () => { const code = 'Object.setPrototypeOf(target, malicious);'; const results = getPrototypePollutionResults(code); expect(results).toHaveLength(1); expect(results[0].api).toBe('Object.setPrototypeOf()'); expect(results[0].riskLevel).toBe('high'); }); test('should detect Object.getPrototypeOf as medium risk', () => { const code = 'Object.getPrototypeOf(obj);'; const results = getPrototypePollutionResults(code); expect(results).toHaveLength(1); expect(results[0].api).toBe('Object.getPrototypeOf()'); expect(results[0].riskLevel).toBe('medium'); }); test('should detect Reflect prototype methods', () => { const code = ` Reflect.setPrototypeOf(target, evil); Reflect.getPrototypeOf(obj); `; const results = getPrototypePollutionResults(code); expect(results).toHaveLength(2); expect(results[0].api).toBe('Reflect.setPrototypeOf()'); expect(results[0].riskLevel).toBe('high'); expect(results[1].api).toBe('Reflect.getPrototypeOf()'); expect(results[1].riskLevel).toBe('medium'); }); }); describe('Object.defineProperty on Prototypes', () => { test('should detect defineProperty on prototypes', () => { const code = 'Object.defineProperty(Object.prototype, "key", {});'; const results = getPrototypePollutionResults(code); expect(results).toHaveLength(1); expect(results[0].api).toBe('Object.defineProperty() on prototype'); expect(results[0].riskLevel).toBe('high'); }); test('should detect defineProperties on prototypes', () => { const code = 'Object.defineProperties(Array.prototype, props);'; const results = getPrototypePollutionResults(code); expect(results).toHaveLength(1); expect(results[0].api).toBe('Object.defineProperties() on prototype'); expect(results[0].riskLevel).toBe('high'); }); test('should detect Reflect.defineProperty on prototypes', () => { const code = 'Reflect.defineProperty(Function.prototype, key, desc);'; const results = getPrototypePollutionResults(code); expect(results).toHaveLength(1); expect(results[0].api).toBe('Reflect.defineProperty() on prototype'); expect(results[0].riskLevel).toBe('high'); }); test('should detect defineProperty with __proto__ key', () => { const code = 'Object.defineProperty(obj, "__proto__", {value: evil});'; const results = getPrototypePollutionResults(code); expect(results.length).toBeGreaterThan(0); expect(results.some(r => r.api === 'Object.defineProperty() with __proto__ key')).toBe(true); expect(results.some(r => r.riskLevel === 'high')).toBe(true); }); test('should detect defineProperty with constructor key', () => { const code = 'Object.defineProperty(target, "constructor", {value: fake});'; const results = getPrototypePollutionResults(code); expect(results.length).toBeGreaterThan(0); expect(results.some(r => r.api === 'Object.defineProperty() with constructor key')).toBe(true); }); test('should detect Reflect.defineProperty with dangerous keys', () => { const code = ` Reflect.defineProperty(obj, "__proto__", descriptor); Reflect.defineProperty(target, "constructor", config); `; const results = getPrototypePollutionResults(code); expect(results.length).toBeGreaterThan(0); expect(results.some(r => r.api.includes('__proto__'))).toBe(true); expect(results.some(r => r.api.includes('constructor'))).toBe(true); }); test('should detect defineProperty with prototype variables', () => { const code = ` const proto = Object.prototype; Object.defineProperty(proto, "polluted", {value: true}); `; const results = getPrototypePollutionResults(code); expect(results.length).toBeGreaterThan(0); expect(results.some(r => r.riskLevel === 'high')).toBe(true); }); }); describe('Dynamic Property Access Patterns', () => { test('should detect dynamic constructor access', () => { const code = 'obj[key]["constructor"] = fake;'; const results = getPrototypePollutionResults(code); expect(results).toHaveLength(1); expect(results[0].api).toBe('Dynamic constructor access'); expect(results[0].riskLevel).toBe('medium'); }); test('should detect dynamic __proto__ access', () => { const code = 'target[prop]["__proto__"] = evil;'; const results = getPrototypePollutionResults(code); expect(results.length).toBeGreaterThan(0); expect(results.some(r => r.api.includes('__proto__'))).toBe(true); }); test('should detect dynamic prototype access', () => { const code = 'item[method]["prototype"] = hack;'; const results = getPrototypePollutionResults(code); expect(results).toHaveLength(1); expect(results[0].api).toBe('Dynamic prototype access'); expect(results[0].riskLevel).toBe('medium'); }); }); describe('For-in Loops with Dynamic Assignment', () => { test('should detect for-in loop pollution patterns', () => { const code = ` for (let key in user) { target[key] = user[key]; } for (const prop in data) { obj[prop] = data[prop]; } `; const results = getPrototypePollutionResults(code); expect(results).toHaveLength(2); results.forEach(result => { expect(result.api).toBe('for-in loop with dynamic assignment'); expect(result.riskLevel).toBe('medium'); }); }); }); describe('JSON.parse with Dangerous Properties', () => { test('should detect JSON.parse with __proto__', () => { const code = 'JSON.parse(\'{"__proto__": {"admin": true}}\');'; const results = getPrototypePollutionResults(code); expect(results).toHaveLength(1); expect(results[0].api).toBe('JSON.parse() with __proto__'); expect(results[0].riskLevel).toBe('high'); }); test('should detect JSON.parse with constructor', () => { const code = 'JSON.parse(data + \',"constructor": {"prototype": {}}\');'; const results = getPrototypePollutionResults(code); expect(results.length).toBeGreaterThan(0); expect(results.some(r => r.api === 'JSON.parse() with constructor')).toBe(true); }); test('should detect JSON.parse with prototype', () => { const code = 'JSON.parse(\'{"prototype": {"exploit": true}}\');'; const results = getPrototypePollutionResults(code); expect(results).toHaveLength(1); expect(results[0].api).toBe('JSON.parse() with prototype'); expect(results[0].riskLevel).toBe('medium'); }); }); describe('Object.assign on Prototypes', () => { test('should detect Object.assign on prototypes', () => { const code = 'Object.assign(Object.prototype, userInput);'; const results = getPrototypePollutionResults(code); expect(results).toHaveLength(2); // Both patterns match expect(results.every(r => r.riskLevel === 'high')).toBe(true); expect(results.some(r => r.api === 'Object.assign() on prototype')).toBe(true); expect(results.some(r => r.api === '.assign() on prototype')).toBe(true); }); test('should detect lodash assign on prototype', () => { const code = '_.assign(String.prototype, malicious);'; const results = getPrototypePollutionResults(code); expect(results).toHaveLength(1); expect(results[0].api).toBe('.assign() on prototype'); expect(results[0].riskLevel).toBe('high'); }); test('should detect Object.assign with multiple sources', () => { const code = 'Object.assign(Array.prototype, source1, source2, userInput);'; const results = getPrototypePollutionResults(code); expect(results.length).toBeGreaterThan(0); expect(results.some(r => r.riskLevel === 'high')).toBe(true); }); test('should detect Object.assign with __proto__ key', () => { const code = 'Object.assign({}, {"__proto__": {"admin": true}});'; const results = getPrototypePollutionResults(code); expect(results.length).toBeGreaterThan(0); expect(results.some(r => r.api.includes('__proto__'))).toBe(true); }); test('should detect Object.assign with constructor key', () => { const code = 'Object.assign(target, {"constructor": {"prototype": {"polluted": true}}});'; const results = getPrototypePollutionResults(code); expect(results.length).toBeGreaterThan(0); expect(results.some(r => r.api.includes('constructor'))).toBe(true); }); test('should detect nested Object.assign on prototypes', () => { const code = ` const proto = Object.prototype; Object.assign(proto, maliciousPayload); `; const results = getPrototypePollutionResults(code); expect(results.length).toBeGreaterThan(0); expect(results.some(r => r.riskLevel === 'high')).toBe(true); }); }); describe('Proxy Traps', () => { test('should detect Proxy with set trap', () => { const code = 'new Proxy(obj, { set: function() {} });'; const results = getPrototypePollutionResults(code); expect(results).toHaveLength(1); expect(results[0].api).toBe('Proxy with set trap'); expect(results[0].riskLevel).toBe('medium'); }); test('should detect Proxy with defineProperty trap', () => { const code = 'new Proxy(target, { defineProperty: handler });'; const results = getPrototypePollutionResults(code); expect(results).toHaveLength(1); expect(results[0].api).toBe('Proxy with defineProperty trap'); expect(results[0].riskLevel).toBe('medium'); }); test('should detect Proxy with setPrototypeOf trap', () => { const code = 'new Proxy(obj, { setPrototypeOf: evil });'; const results = getPrototypePollutionResults(code); expect(results).toHaveLength(1); expect(results[0].api).toBe('Proxy with setPrototypeOf trap'); expect(results[0].riskLevel).toBe('high'); }); test('should detect multiple proxy traps', () => { const code = 'new Proxy(a, { set: h1, defineProperty: h2, setPrototypeOf: h3 });'; const results = getPrototypePollutionResults(code); expect(results).toHaveLength(3); expect(results.filter(r => r.riskLevel === 'high')).toHaveLength(1); // setPrototypeOf expect(results.filter(r => r.riskLevel === 'medium')).toHaveLength(2); // set and defineProperty }); }); describe('Complex Real-world Scenarios', () => { test('should detect vulnerable merge function', () => { const code = ` function merge(target, source) { for (let key in source) { target[key] = source[key]; } } `; const results = getPrototypePollutionResults(code); expect(results).toHaveLength(1); expect(results[0].api).toBe('for-in loop with dynamic assignment'); expect(results[0].riskLevel).toBe('medium'); }); test('should detect config processing vulnerability', () => { const code = ` const config = JSON.parse('{"__proto__": {"admin": true}}'); const result = {}; for (let prop in config) { result[prop] = config[prop]; } `; const results = getPrototypePollutionResults(code); expect(results).toHaveLength(2); expect(results.some(r => r.api === 'JSON.parse() with __proto__')).toBe(true); expect(results.some(r => r.api === 'for-in loop with dynamic assignment')).toBe(true); }); test('should detect multiple pollution patterns', () => { const code = ` Object.prototype.polluted = true; obj.__proto__ = evil; Object.setPrototypeOf(target, malicious); for (let k in user) { obj[k] = user[k]; } `; const results = getPrototypePollutionResults(code); expect(results).toHaveLength(4); expect(results.filter(r => r.riskLevel === 'high')).toHaveLength(3); expect(results.filter(r => r.riskLevel === 'medium')).toHaveLength(1); }); }); describe('False Positive Prevention', () => { test('should not detect safe object operations', () => { const code = ` const obj = { name: "test" }; obj.value = 42; obj.method = function() { return this.value; }; `; const results = getPrototypePollutionResults(code); expect(results).toHaveLength(0); }); test('should not detect safe prototype reading', () => { const code = ` const method = Object.prototype.toString; Array.prototype.slice.call(args); console.log(Array.prototype.length); `; const results = getPrototypePollutionResults(code); expect(results).toHaveLength(0); }); test('should not detect safe for-in without assignment', () => { const code = ` for (let key in obj) { console.log(key, obj[key]); } `; const results = getPrototypePollutionResults(code); expect(results).toHaveLength(0); }); test('should not detect safe JSON operations', () => { const code = ` const data = JSON.parse('{"normal": "data"}'); JSON.stringify({key: "value"}); `; const results = getPrototypePollutionResults(code); expect(results).toHaveLength(0); }); test('should not detect safe proxy usage', () => { const code = ` new Proxy(obj, { get: function(target, prop) { return target[prop]; } }); `; const results = getPrototypePollutionResults(code); expect(results).toHaveLength(0); }); }); describe('Other Assignment Variant Methods', () => { test('should detect Object.create with polluted prototype', () => { const code = 'Object.create(null, {"__proto__": {value: evil}});'; const results = getPrototypePollutionResults(code); expect(results.length).toBeGreaterThan(0); expect(results.some(r => r.api.includes('__proto__'))).toBe(true); }); test('should detect spread operator with prototype pollution', () => { const code = 'const obj = {...userInput, "__proto__": malicious};'; const results = getPrototypePollutionResults(code); expect(results.length).toBeGreaterThan(0); expect(results.some(r => r.api.includes('__proto__'))).toBe(true); }); test('should detect Object.fromEntries with dangerous keys', () => { const code = 'Object.fromEntries([["__proto__", evil], ["constructor", hack]]);'; const results = getPrototypePollutionResults(code); expect(results.length).toBeGreaterThan(0); expect(results.some(r => r.api.includes('__proto__') || r.api.includes('constructor'))).toBe(true); }); test('should detect mixin pattern with prototype pollution', () => { const code = ` function mixin(target, source) { Object.assign(target.prototype, source); } `; const results = getPrototypePollutionResults(code); expect(results.length).toBeGreaterThan(0); expect(results.some(r => r.riskLevel === 'high')).toBe(true); }); test('should detect jQuery extend on prototypes', () => { const code = '$.extend(Object.prototype, userPayload);'; const results = getPrototypePollutionResults(code); expect(results.length).toBeGreaterThan(0); expect(results.some(r => r.api.includes('extend') || r.api.includes('prototype'))).toBe(true); }); test('should detect deep extend patterns', () => { const code = '_.extend(true, Array.prototype, maliciousObject);'; const results = getPrototypePollutionResults(code); expect(results.length).toBeGreaterThan(0); expect(results.some(r => r.riskLevel === 'high')).toBe(true); }); test('should detect merge variants', () => { const code = ` _.merge(Function.prototype, evilPayload); jQuery.extend(true, String.prototype, hack); `; const results = getPrototypePollutionResults(code); expect(results.length).toBeGreaterThan(0); expect(results.some(r => r.riskLevel === 'high')).toBe(true); }); test('should detect Ramda assoc on prototypes', () => { const code = 'R.assoc("polluted", true, Object.prototype);'; const results = getPrototypePollutionResults(code); expect(results.length).toBeGreaterThan(0); expect(results.some(r => r.api.includes('prototype'))).toBe(true); }); test('should detect immutable update libraries misuse', () => { const code = 'update(Object.prototype, {$merge: userInput});'; const results = getPrototypePollutionResults(code); expect(results.length).toBeGreaterThan(0); expect(results.some(r => r.api.includes('prototype'))).toBe(true); }); }); describe('Edge Cases', () => { test('should handle whitespace variations', () => { const code = 'Object . prototype [ "key" ] = value;'; const results = getPrototypePollutionResults(code); expect(results).toHaveLength(1); expect(results[0].riskLevel).toBe('high'); }); test('should handle minified code', () => { const code = 'Object.prototype[a]=b,Array.prototype[c]=d;'; const results = getPrototypePollutionResults(code); expect(results).toHaveLength(2); results.forEach(result => { expect(result.riskLevel).toBe('high'); }); }); test('should handle mixed quotes', () => { const code = `obj['__proto__']["key"] = evil;`; const results = getPrototypePollutionResults(code); expect(results.length).toBeGreaterThan(0); expect(results.some(r => r.api.includes('__proto__'))).toBe(true); }); }); });