@nodesecure/js-x-ray
Version:
JavaScript AST XRay analysis
145 lines • 4.02 kB
JavaScript
// Import Internal Dependencies
import { getCallExpressionIdentifier, getMemberExpressionIdentifier } from "../estree/index.js";
import { generateWarning } from "../warnings.js";
// CONSTANTS
export const JS_TYPES = new Set([
"AggregateError",
"Array",
"ArrayBuffer",
"BigInt",
"BigInt64Array",
"BigUint64Array",
"Boolean",
"DataView",
"Date",
"Error",
"EvalError",
"FinalizationRegistry",
"Float32Array",
"Float64Array",
"Function",
"Int16Array",
"Int32Array",
"Int8Array",
"Map",
"Number",
"Object",
"Promise",
"Proxy",
"RangeError",
"ReferenceError",
"Reflect",
"RegExp",
"Set",
"SharedArrayBuffer",
"String",
"Symbol",
"SyntaxError",
"TypeError",
"Uint16Array",
"Uint32Array",
"Uint8Array",
"Uint8ClampedArray",
"URIError",
"WeakMap",
"WeakRef",
"WeakSet"
]);
/**
* @description Search for monkey patching of built-in prototypes.
* @example
* Array.prototype.map = function() {};
*/
function validateNodeAssignment(node, ctx) {
if (node.type !== "AssignmentExpression" ||
node.left.type !== "MemberExpression") {
return [false];
}
return validateMemberExpression(node.left, ctx);
}
function resolveDefinePropertyIdentifier(node, ctx) {
const id = getCallExpressionIdentifier(node);
if (id === "Object.defineProperty" || id === "Reflect.defineProperty") {
return id;
}
if (id === null || !id.includes(".")) {
return null;
}
const [objectPart, ...rest] = id.split(".");
const methodName = rest.join(".");
if (methodName !== "defineProperty") {
return null;
}
const resolved = resolveJsTypeName(objectPart, ctx);
if (resolved === "Object" || resolved === "Reflect") {
return `${resolved}.defineProperty`;
}
return null;
}
function validateDefineProperty(node, ctx) {
if (node.type !== "CallExpression") {
return [false];
}
const resolvedId = resolveDefinePropertyIdentifier(node, ctx);
if (resolvedId === null) {
return [false];
}
// TODO: detect aliased prototype target in defineProperty,
// e.g. const ap = Array.prototype; Object.defineProperty(ap, ...)
const firstArg = node.arguments.at(0);
if (firstArg?.type !== "MemberExpression") {
return [false];
}
return validateMemberExpression(firstArg, ctx);
}
function resolveJsTypeName(name, ctx) {
if (JS_TYPES.has(name)) {
return name;
}
const tracedData = ctx.sourceFile.tracer.getDataFromIdentifier(name);
if (tracedData !== null && JS_TYPES.has(tracedData.identifierOrMemberExpr)) {
return tracedData.identifierOrMemberExpr;
}
return null;
}
function validateMemberExpression(node, ctx) {
const iter = getMemberExpressionIdentifier(node, {
externalIdentifierLookup: (name) => ctx.sourceFile.tracer.literalIdentifiers.get(name)?.value ?? null
});
const rawName = iter.next().value;
if (typeof rawName !== "string") {
return [false];
}
const jsTypeName = resolveJsTypeName(rawName, ctx);
if (jsTypeName === null) {
return [false];
}
return [
iter.next().value === "prototype",
`${jsTypeName}.prototype`
];
}
function initialize(ctx) {
const { tracer } = ctx.sourceFile;
for (const jsType of JS_TYPES) {
tracer.trace(jsType, {
followConsecutiveAssignment: true
});
}
}
function main(node, options) {
const { sourceFile, data: prototypeName } = options;
sourceFile.warnings.push(generateWarning("monkey-patch", { value: prototypeName, location: node.loc }));
}
export default {
name: "isMonkeyPatch",
nodeTypes: ["AssignmentExpression", "CallExpression"],
validateNode: [
validateNodeAssignment,
validateDefineProperty
],
main,
initialize,
breakOnMatch: false
};
//# sourceMappingURL=isMonkeyPatch.js.map