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
text/typescript
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);
});
});
});