Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 | 4x 5x 5x 5x 5x 2x 2x 2x 2x 18x 15x 3x 3x 3x 3x 3x 2x 2x 2x 2x 2x 2x 2x 4x 2x 85x 52x 2x 2x 52x 4x 4x 5x 5x 5x 1153x 85x 85x | import * as ts from 'typescript';
import { isFunctionLike } from './ast-helpers.js';
import { findParentBlock } from './collect-effects.js';
import { getLineAndCharacter } from './utils.js';
const DEEP_MERGE_NAMES = new Set([
'merge', 'deepMerge', 'deepAssign', 'extend', 'deepExtend',
'defaults', 'defaultsDeep', 'assign', 'mixin',
]);
/** Check if a computed-property-write key comes from a for..of/for..in loop over known internal iteration */
function isKeyFromInternalIteration(node: ts.ElementAccessExpression, sourceFile: ts.SourceFile): boolean {
const keyExpr = node.argumentExpression;
Iif (!keyExpr || !ts.isIdentifier(keyExpr)) return false;
const keyName = keyExpr.getText(sourceFile);
// Walk up to find a for-of/for-in loop that declares this key
let current: ts.Node | undefined = node.parent;
while (current) {
if (ts.isForOfStatement(current) || ts.isForInStatement(current)) {
const init = current.initializer;
if (init) {
const initText = init.getText(sourceFile);
if (initText.includes(keyName)) {
// Check if the iterable is a known-safe internal source
const expr = current.expression.getText(sourceFile);
if (/Object\.(keys|values|entries|getOwnPropertyNames)\(/.test(expr) ||
/\.keys\(\)|\.values\(\)|\.entries\(\)/.test(expr) ||
/Array\.from\(/.test(expr)) {
return true;
}
}
}
}
if (isFunctionLike(current)) break;
current = current.parent;
}
return false;
}
/** Check if the containing block has a __proto__/constructor/prototype key guard */
function hasProtoKeyGuard(node: ts.Node, sourceFile: ts.SourceFile): boolean {
const block = findParentBlock(node);
Iif (!block) return false;
const blockText = block.getText(sourceFile);
return /__proto__|constructor|prototype/.test(blockText) &&
(blockText.includes('===') || blockText.includes('!==') ||
blockText.includes('includes(') || blockText.includes('hasOwnProperty'));
}
/** Check if the target object was created with Object.create(null) or is Map/Set */
function isTargetSafeObject(node: ts.ElementAccessExpression, sourceFile: ts.SourceFile): boolean {
const objText = node.expression.getText(sourceFile);
// Walk up to find variable declaration
let current: ts.Node | undefined = node.parent;
while (current) {
if (ts.isBlock(current) || ts.isSourceFile(current)) {
// Search for Object.create(null) or new Map/Set assignment for this object
const text = current.getText(sourceFile);
const createNullPattern = new RegExp(`${objText.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*=\\s*Object\\.create\\(null\\)`);
const mapSetPattern = new RegExp(`${objText.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*=\\s*new\\s+(Map|Set)\\b`);
Iif (createNullPattern.test(text) || mapSetPattern.test(text)) return true;
break;
}
current = current.parent;
}
return false;
}
export function collectPrototypePollutionSites(
sourceFile: ts.SourceFile,
): Array<{ kind: string; detail: string; lineStart: number; lineEnd: number; guarded: boolean }> {
const sites: Array<{ kind: string; detail: string; lineStart: number; lineEnd: number; guarded: boolean }> = [];
const visit = (node: ts.Node): void => {
if (ts.isCallExpression(node)) {
const text = node.expression.getText(sourceFile);
if (text === 'Object.assign' && node.arguments.length >= 2) {
const loc = getLineAndCharacter(sourceFile, node);
sites.push({ kind: 'object-assign', detail: `Object.assign() merges properties without __proto__ guard`, lineStart: loc.lineStart, lineEnd: loc.lineEnd, guarded: false });
}
const calleeName = text.split('.').pop() || '';
if (DEEP_MERGE_NAMES.has(calleeName) && node.arguments.length >= 1) {
const loc = getLineAndCharacter(sourceFile, node);
sites.push({ kind: 'deep-merge', detail: `${calleeName}() deep-merges without prototype guard`, lineStart: loc.lineStart, lineEnd: loc.lineEnd, guarded: false });
}
}
if (
ts.isElementAccessExpression(node) &&
node.argumentExpression &&
!ts.isStringLiteral(node.argumentExpression) &&
!ts.isNumericLiteral(node.argumentExpression) &&
node.parent && ts.isBinaryExpression(node.parent) &&
node.parent.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
node.parent.left === node
) {
const guarded = isKeyFromInternalIteration(node, sourceFile) ||
hasProtoKeyGuard(node, sourceFile) ||
isTargetSafeObject(node, sourceFile);
const loc = getLineAndCharacter(sourceFile, node);
sites.push({ kind: 'computed-property-write', detail: `Dynamic bracket assignment: ${node.getText(sourceFile).slice(0, 40)}`, lineStart: loc.lineStart, lineEnd: loc.lineEnd, guarded });
}
ts.forEachChild(node, visit);
};
ts.forEachChild(sourceFile, visit);
return sites;
}
|