UNPKG

@nodesecure/js-x-ray

Version:
122 lines 4.28 kB
import { CALL_EXPRESSION_DATA } from "../../contants.js"; import { isCallExpression, isFunctionNode, isIdentifier, isMemberExpression } from "../../estree/types.js"; import { getParamNames, getMemberCallExpression } from "../../estree/index.js"; import { generateWarning } from "../../warnings.js"; import { VariableTracer } from "../../VariableTracer.js"; const kModuleName = "bcryptjs"; const kTracedFunctions = new Set(["bcryptjs.hash", "bcryptjs.hashSync"]); const kShuckingVariables = Symbol("shuckingVariables"); const kAmbiguousVariableNames = Symbol("ambiguousVariableNames"); /** * createHmac is intentionally excluded, HMAC with a pepper is the OWASP-safe pattern. */ const kHashDigestChains = [ "crypto.createHash.update.digest", "crypto.createHash.update.digest.toString", "crypto.createHash.digest", "crypto.createHash.digest.toString" ]; function isCreateHashChain(node) { let current = node; while (isCallExpression(current)) { const { callee } = current; if (!isMemberExpression(callee)) { break; } if (isIdentifier(callee.property) && callee.property.name === "createHash") { return true; } current = callee.object; } return false; } function hasDigestChain(hashNode) { if (getMemberCallExpression(hashNode, "digest") !== null) { return true; } const toStringCall = getMemberCallExpression(hashNode, "toString"); if (toStringCall) { return getMemberCallExpression(toStringCall.callee.object, "digest") !== null; } return false; } function isShuckingPrehash(hashNode) { return hasDigestChain(hashNode) && isCreateHashChain(hashNode); } function validateNode(node, ctx) { const { tracer } = ctx.sourceFile; if (!tracer.importedModules.has(kModuleName) || !tracer.importedModules.has("crypto")) { return [false]; } if (isFunctionNode(node)) { const paramNames = getParamNames(node.params); const shuckingVars = ctx.context[kShuckingVariables]; if (paramNames.some((name) => shuckingVars.has(name))) { ctx.setEntryPoint("markAmbiguousParams"); return [true, paramNames]; } return [false]; } return [kTracedFunctions.has(ctx.context[CALL_EXPRESSION_DATA]?.identifierOrMemberExpr)]; } function initialize(ctx) { const { tracer } = ctx.sourceFile; ctx.context[kShuckingVariables] = new Set(); ctx.context[kAmbiguousVariableNames] = new Set(); for (const fn of kTracedFunctions) { tracer.trace(fn, { followConsecutiveAssignment: true, moduleName: kModuleName }); } for (const chain of kHashDigestChains) { tracer.trace(chain, { followReturnValueAssignement: true, followConsecutiveAssignment: true, moduleName: "crypto" }); } tracer.on(VariableTracer.ReturnValueEvent, (payload) => { if (!kHashDigestChains.includes(payload.identifierOrMemberExpr)) { return; } ctx.context[kShuckingVariables].add(payload.id); }); } function markAmbiguousParams(_node, ctx) { for (const name of ctx.data) { ctx.context[kAmbiguousVariableNames].add(name); } } function bcryptHashCall(bcryptNode, ctx) { const hashArgument = bcryptNode.arguments.at(0); const ambiguousVars = ctx.context[kAmbiguousVariableNames]; const shuckingVars = ctx.context[kShuckingVariables]; const isVariableShucking = isIdentifier(hashArgument) && !ambiguousVars.has(hashArgument.name) && shuckingVars.has(hashArgument.name); if (isVariableShucking || isShuckingPrehash(hashArgument)) { ctx.sourceFile.warnings.push(generateWarning("crypto.password-shucking", { value: null, location: bcryptNode.loc })); } } export default { name: "isPasswordShucking", nodeTypes: [ "CallExpression", "FunctionDeclaration", "FunctionExpression", "ArrowFunctionExpression" ], validateNode, main: { default: bcryptHashCall, markAmbiguousParams }, initialize, breakOnMatch: false, context: {} }; //# sourceMappingURL=isPasswordShucking.js.map