UNPKG

@nodesecure/js-x-ray

Version:
463 lines 19.4 kB
// Import Node.js Dependencies import { EventEmitter } from "node:events"; // Import Third-party Dependencies import {} from "meriyah"; import { match } from "ts-pattern"; // Import Internal Dependencies import { extractLogicalExpression, getCallExpressionArguments, getCallExpressionIdentifier, getMemberExpressionIdentifier, getVariableDeclarationIdentifiers, isLiteral, toLiteral } from "./estree/index.js"; import { getSubMemberExpressionSegments, isEvilIdentifierPath, isNeutralCallable, makePrefixRemover, notNullOrUndefined, stripNodePrefix } from "./utils/index.js"; // CONSTANTS const kGlobalIdentifiersToTrace = new Set([ "globalThis", "global", "root", "GLOBAL", "window" ]); const kGlobalIdentifiersRemover = makePrefixRemover(kGlobalIdentifiersToTrace); const kRequirePatterns = new Set([ "require", "require.resolve", "require.main", "process.mainModule.require", "process.getBuiltinModule" ]); const kUnsafeGlobalCallExpression = new Set(["eval", "Function"]); export class VariableTracer extends EventEmitter { static AssignmentEvent = Symbol("AssignmentEvent"); static ImportEvent = Symbol("ImportEvent"); // PUBLIC PROPERTIES literalIdentifiers = new Map(); importedModules = new Set(); // PRIVATE PROPERTIES #traced = new Map(); #variablesRefToGlobal = new Set(); #neutralCallable = new Set(); #assignedReturnValueToTraced = new Map(); debug() { console.log(this.#traced); } enableDefaultTracing() { [...kRequirePatterns] .forEach((pattern) => this.trace(pattern, { followConsecutiveAssignment: true, name: "require" })); return this .trace("eval") .trace("Function") .trace("atob", { followConsecutiveAssignment: true }); } /** * @example * new VariableTracer() * .trace("require", { followConsecutiveAssignment: true }) * .trace("process.mainModule") */ trace(identifierOrMemberExpr, options = {}) { const { followConsecutiveAssignment = false, followReturnValueAssignement = false, moduleName = null, name = identifierOrMemberExpr } = options; this.#traced.set(identifierOrMemberExpr, { name, identifierOrMemberExpr, followConsecutiveAssignment, followReturnValueAssignement, assignmentMemory: [], moduleName }); if (identifierOrMemberExpr.includes(".")) { const exprs = [...getSubMemberExpressionSegments(identifierOrMemberExpr)] .filter((expr) => !this.#traced.has(expr)); for (const expr of exprs) { this.trace(expr, { followConsecutiveAssignment: true, name, moduleName }); } } return this; } getDataFromIdentifier(identifierOrMemberExpr, options = {}) { const { removeGlobalIdentifier = false } = options; if (removeGlobalIdentifier) { // eslint-disable-next-line no-param-reassign identifierOrMemberExpr = kGlobalIdentifiersRemover(identifierOrMemberExpr); } const isMemberExpr = identifierOrMemberExpr.includes("."); const isTracingIdentifier = this.#traced.has(identifierOrMemberExpr); let finalIdentifier = identifierOrMemberExpr; if (isMemberExpr && !isTracingIdentifier) { const [segment] = identifierOrMemberExpr.split("."); if (this.#traced.has(segment)) { const tracedIdentifier = this.#traced.get(segment); finalIdentifier = `${tracedIdentifier.identifierOrMemberExpr}${identifierOrMemberExpr.slice(segment.length)}`; } if (!this.#traced.has(finalIdentifier)) { return null; } } else if (!isTracingIdentifier) { return null; } const tracedIdentifier = this.#traced.get(finalIdentifier); if (!this.#isTracedIdentifierImportedAsModule(tracedIdentifier)) { return null; } const assignmentMemory = this.#traced.get(tracedIdentifier.name)?.assignmentMemory ?? []; return { name: tracedIdentifier.name, identifierOrMemberExpr: tracedIdentifier.identifierOrMemberExpr, assignmentMemory }; } #getTracedName(identifierOrMemberExpr) { return this.#traced.get(identifierOrMemberExpr)?.name ?? null; } #isTracedIdentifierImportedAsModule(id) { return id.moduleName === null || this.importedModules.has(id.moduleName); } #declareNewAssignment(identifierOrMemberExpr, id) { const tracedVariant = this.#traced.get(identifierOrMemberExpr); // We return if required module has not been imported // It mean the assigment has no relation with the required tracing if (typeof tracedVariant === "undefined" || !this.#isTracedIdentifierImportedAsModule(tracedVariant)) { return; } const newIdentiferName = id.name; const assignmentEventPayload = { name: tracedVariant.name, identifierOrMemberExpr: tracedVariant.identifierOrMemberExpr, id: newIdentiferName, location: id.loc }; this.emit(VariableTracer.AssignmentEvent, assignmentEventPayload); this.emit(tracedVariant.identifierOrMemberExpr, assignmentEventPayload); if (tracedVariant.followConsecutiveAssignment && !this.#traced.has(newIdentiferName)) { this.#traced.get(tracedVariant.name).assignmentMemory.push({ type: "AliasBinding", name: newIdentiferName }); this.#traced.set(newIdentiferName, tracedVariant); } } #isGlobalVariableIdentifier(identifierName) { return kGlobalIdentifiersToTrace.has(identifierName) || this.#variablesRefToGlobal.has(identifierName); } /** * Search alternative for the given MemberExpression parts * * @example * const { process: aName } = globalThis; * const boo = aName.mainModule.require; // alternative: process.mainModule.require */ #searchForMemberExprAlternative(parts = []) { return parts.flatMap((identifierName) => { if (this.#traced.has(identifierName)) { return this.#traced.get(identifierName).identifierOrMemberExpr; } /** * If identifier is global then we can eliminate the value from MemberExpr * * globalThis.process === process; */ if (this.#isGlobalVariableIdentifier(identifierName)) { return []; } return identifierName; }); } #autoTraceId(id, prefix = null) { for (const { name, assignmentId } of getVariableDeclarationIdentifiers(id)) { const identifierOrMemberExpr = typeof prefix === "string" ? `${prefix}.${name}` : name; if (this.#traced.has(identifierOrMemberExpr)) { this.#declareNewAssignment(identifierOrMemberExpr, assignmentId); } } } #reverseAtob(node, id) { const callExprArguments = getCallExpressionArguments(node, { externalIdentifierLookup: (name) => this.literalIdentifiers.get(name)?.value ?? null }); if (callExprArguments === null) { return; } const callExprArgumentNode = callExprArguments.at(0); if (typeof callExprArgumentNode === "string") { this.literalIdentifiers.set(id.name, { value: Buffer.from(callExprArgumentNode, "base64").toString(), type: "Literal" }); } } #walkImportDeclaration(node) { const moduleName = stripNodePrefix(node.source.value) .replace(/\/promises$/, ""); if (!this.#traced.has(moduleName)) { return; } this.importedModules.add(moduleName); this.emit(VariableTracer.ImportEvent, { moduleName, value: node.source.value, location: node.loc }); // import * as boo from "crypto"; if (node.specifiers[0].type === "ImportNamespaceSpecifier") { const importNamespaceNode = node.specifiers[0]; this.#declareNewAssignment(moduleName, importNamespaceNode.local); return; } // import { createHash } from "crypto"; const importSpecifiers = node.specifiers .filter((specifierNode) => specifierNode.type === "ImportSpecifier"); for (const specifier of importSpecifiers) { if (specifier.imported.type !== "Identifier") { continue; } const fullImportedName = `${moduleName}.${specifier.imported.name}`; if (this.#traced.has(fullImportedName)) { this.#declareNewAssignment(fullImportedName, specifier.imported); } } } #walkRequireCallExpression(node, id) { const moduleNameLiteral = node.arguments .find((argumentNode) => isLiteral(argumentNode) && this.#traced.has(stripNodePrefix(argumentNode.value))); if (!moduleNameLiteral) { return; } const moduleName = stripNodePrefix(moduleNameLiteral.value); this.importedModules.add(moduleName); this.emit(VariableTracer.ImportEvent, { moduleName, value: moduleNameLiteral.value, location: moduleNameLiteral.loc }); switch (id.type) { case "Identifier": this.#declareNewAssignment(moduleName, id); break; case "ObjectPattern": { this.#autoTraceId(id, moduleName); break; } } } #walkVariableDeclaratorInitialization(variableDeclaratorNode, childNode = variableDeclaratorNode.init) { if (childNode === null) { return; } const { id } = variableDeclaratorNode; if (id.type !== "Identifier") { return; } switch (childNode.type) { // let foo = "10"; <-- "foo" is the key and "10" the value case "Literal": { this.literalIdentifiers.set(id.name, { value: String(childNode.value), type: childNode.type }); break; } // const x = `hello ${name}`; "x" is the key and "hello ${0}" the value case "TemplateLiteral": { this.literalIdentifiers.set(id.name, { value: toLiteral(childNode), type: childNode.type }); break; } /** * import os from "node:os"; * * const foo = { * host: os.hostname(), <-- Property * ...{ bar: "hello world"} <-- SpreadElement * }; * ^ ObjectExpression */ case "ObjectExpression": { for (const property of childNode.properties) { const node = match(property) .with({ type: "Property" }, (prop) => prop.value) .with({ type: "SpreadElement" }, (prop) => prop.argument) .otherwise(() => null); node && this.#walkVariableDeclaratorInitialization(variableDeclaratorNode, node); } break; } case "ArrayExpression": { for (const element of childNode.elements) { this.#walkVariableDeclaratorInitialization(variableDeclaratorNode, element); } break; } case "SpreadElement": { this.#walkVariableDeclaratorInitialization(variableDeclaratorNode, childNode.argument); break; } /** * const g = eval("this"); * const g = Function("return this")(); */ case "CallExpression": { const fullIdentifierPath = getCallExpressionIdentifier(childNode); if (fullIdentifierPath === null) { break; } const tracedFullIdentifierName = this.#getTracedName(fullIdentifierPath) ?? fullIdentifierPath; const [identifierName] = fullIdentifierPath.split("."); const tracedVariant = this.#traced.get(tracedFullIdentifierName); if (tracedVariant?.followReturnValueAssignement) { tracedVariant.assignmentMemory.push({ type: "ReturnValueAssignment", name: id.name }); if (tracedVariant.followConsecutiveAssignment) { this.#assignedReturnValueToTraced.set(id.name, tracedFullIdentifierName); } } // const id = Function.prototype.call.call(require, require, "http"); if (this.#neutralCallable.has(identifierName) || isEvilIdentifierPath(fullIdentifierPath)) { // TODO: make sure we are walking on a require CallExpr here ? this.#walkRequireCallExpression(childNode, id); } else if (kUnsafeGlobalCallExpression.has(identifierName)) { this.#variablesRefToGlobal.add(id.name); } // const foo = require("crypto"); // const bar = require.call(null, "crypto"); else if (kRequirePatterns.has(identifierName)) { this.#walkRequireCallExpression(childNode, id); } else if (tracedFullIdentifierName === "atob") { this.#reverseAtob(childNode, id); } break; } // const r = require case "Identifier": { const identifierName = childNode.name; if (this.#traced.has(identifierName)) { this.#declareNewAssignment(identifierName, id); } else if (this.#isGlobalVariableIdentifier(identifierName)) { this.#variablesRefToGlobal.add(id.name); } if (this.#assignedReturnValueToTraced.has(childNode.name)) { const tracedFullIdentifierName = this.#assignedReturnValueToTraced.get(childNode.name); const tracedVariant = this.#traced.get(tracedFullIdentifierName); tracedVariant.assignmentMemory.push({ type: "ReturnValueAssignment", name: id.name }); this.#assignedReturnValueToTraced.set(id.name, tracedFullIdentifierName); } break; } // process.mainModule and require.resolve case "MemberExpression": { // Example: ["process", "mainModule"] const memberExprParts = [ ...getMemberExpressionIdentifier(childNode, { externalIdentifierLookup: (name) => this.literalIdentifiers.get(name)?.value ?? null }) ]; const memberExprFullname = memberExprParts.join("."); // Function.prototype.call if (isNeutralCallable(memberExprFullname)) { this.#neutralCallable.add(id.name); } else if (this.#traced.has(memberExprFullname)) { this.#declareNewAssignment(memberExprFullname, id); } else { const alternativeMemberExprParts = this.#searchForMemberExprAlternative(memberExprParts); const alternativeMemberExprFullname = alternativeMemberExprParts.join("."); if (this.#traced.has(alternativeMemberExprFullname)) { this.#declareNewAssignment(alternativeMemberExprFullname, id); } } if (childNode.object.type === "CallExpression") { this.#walkVariableDeclaratorInitialization(variableDeclaratorNode, childNode.object); } break; } } } #walkVariableDeclarationWithAnythingElse(variableDeclaratorNode) { const { init } = variableDeclaratorNode; if (init === null) { return; } const { id } = variableDeclaratorNode; switch (init.type) { // const { process } = eval("this"); case "CallExpression": { const fullIdentifierPath = getCallExpressionIdentifier(init); if (fullIdentifierPath === null) { break; } const [identifierName] = fullIdentifierPath.split("."); // const {} = Function.prototype.call.call(require, require, "http"); if (isEvilIdentifierPath(fullIdentifierPath)) { this.#walkRequireCallExpression(init, id); } else if (kUnsafeGlobalCallExpression.has(identifierName)) { this.#autoTraceId(id); } // const { createHash } = require("crypto"); else if (kRequirePatterns.has(identifierName)) { this.#walkRequireCallExpression(init, id); } break; } // const { process } = globalThis; case "Identifier": { const identifierName = init.name; if (this.#isGlobalVariableIdentifier(identifierName)) { this.#autoTraceId(id); } break; } } } #walkVariableDeclarator(node) { // var foo; <-- no initialization here. if (!notNullOrUndefined(node.init)) { return; } /** * const { foo } = {}; * ^ ^ ObjectPattern (example) */ if (node.id.type !== "Identifier") { this.#walkVariableDeclarationWithAnythingElse(node); return; } // var root = freeGlobal || freeSelf || Function('return this')(); if (node.init.type === "LogicalExpression") { for (const extractedNode of extractLogicalExpression(node.init)) { this.#walkVariableDeclaratorInitialization(node, extractedNode.node); } } // const foo = "bar"; else { this.#walkVariableDeclaratorInitialization(node); } } walk(node) { switch (node.type) { case "ImportDeclaration": { this.#walkImportDeclaration(node); break; } case "VariableDeclaration": { node.declarations.forEach((node) => this.#walkVariableDeclarator(node)); break; } } } } //# sourceMappingURL=VariableTracer.js.map