UNPKG

@nodesecure/js-x-ray

Version:
157 lines 5.83 kB
import { CALL_EXPRESSION_DATA } from "../../contants.js"; import { isStringLiteral, isFunctionNode, isIdentifier, isCallExpression } 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 kUnsafeDigestVariables = Symbol("unsafeDigestVariables"); const kAmbiguousVariableNames = Symbol("ambiguousVariableNames"); function getContextSet(ctx, key) { return ctx.context[key]; } /** * Digest encodings that produce ASCII-only output, avoiding the null-byte truncation issue */ const kSafeDigestEncodings = new Set(["base64", "base64url", "hex"]); const kDigestChains = [ "crypto.createHash.update.digest", "crypto.createHash.update.digest.toString", "crypto.createHash.digest", "crypto.createHash.digest.toString", "crypto.createHmac.update.digest", "crypto.createHmac.update.digest.toString", "crypto.createHmac.digest", "crypto.createHmac.digest.toString" ]; /** * Resolves both `x.digest(encoding)` and `x.digest().toString(encoding)` */ function resolveDigestEncodingArguments(hashNode) { const digestCall = getMemberCallExpression(hashNode, "digest"); if (digestCall) { return digestCall.arguments; } const toStringCall = getMemberCallExpression(hashNode, "toString"); if (toStringCall) { const innerDigestCall = getMemberCallExpression(toStringCall.callee.object, "digest"); if (innerDigestCall) { return innerDigestCall.arguments.length === 0 ? toStringCall.arguments : innerDigestCall.arguments; } } return null; } function isSafeEncodingArg(node, literalIdentifiers) { if (isStringLiteral(node)) { return kSafeDigestEncodings.has(node.value); } if (isIdentifier(node)) { const literal = literalIdentifiers.get(node.name); return literal !== undefined && kSafeDigestEncodings.has(literal.value); } return false; } function hasUnsafeDigestEncoding(hashNode, literalIdentifiers) { const encodingArgs = resolveDigestEncodingArguments(hashNode); if (encodingArgs === null) { return false; } return !isSafeEncodingArg(encodingArgs.at(0), literalIdentifiers); } 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 unsafeVars = getContextSet(ctx, kUnsafeDigestVariables); if (paramNames.some((name) => unsafeVars.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[kUnsafeDigestVariables] = new Set(); ctx.context[kAmbiguousVariableNames] = new Set(); for (const identifierOrMemberExpr of kTracedFunctions) { tracer.trace(identifierOrMemberExpr, { followConsecutiveAssignment: true, moduleName: kModuleName }); } for (const chain of kDigestChains) { tracer.trace(chain, { followReturnValueAssignement: true, followConsecutiveAssignment: true, moduleName: "crypto" }); } tracer.on(VariableTracer.ReturnValueEvent, (payload) => { if (!kDigestChains.includes(payload.identifierOrMemberExpr)) { return; } const encodingArg = payload.arguments.at(0); if (!isSafeEncodingArg(encodingArg, tracer.literalIdentifiers)) { ctx.context[kUnsafeDigestVariables].add(payload.id); } }); } function markAmbiguousParams(_node, ctx) { const ambiguousVariableNames = getContextSet(ctx, kAmbiguousVariableNames); for (const name of ctx.data) { ambiguousVariableNames.add(name); } } function bcryptHashCall(bcryptNode, ctx) { const { sourceFile } = ctx; const hashArgument = bcryptNode.arguments.at(0); let isUnsafe; if (isIdentifier(hashArgument)) { const isAmbiguous = getContextSet(ctx, kAmbiguousVariableNames).has(hashArgument.name); const isDigestVariable = getContextSet(ctx, kUnsafeDigestVariables).has(hashArgument.name); isUnsafe = !isAmbiguous && isDigestVariable; } else if (isCallExpression(hashArgument) && isIdentifier(hashArgument.callee) && !getContextSet(ctx, kAmbiguousVariableNames).has(hashArgument.callee.name) && getContextSet(ctx, kUnsafeDigestVariables).has(hashArgument.callee.name)) { const encodingArg = hashArgument.arguments.at(0); isUnsafe = !isSafeEncodingArg(encodingArg, sourceFile.tracer.literalIdentifiers); } else { isUnsafe = hasUnsafeDigestEncoding(hashArgument, sourceFile.tracer.literalIdentifiers); } if (isUnsafe) { sourceFile.warnings.push(generateWarning("crypto.unsafe-prehash", { value: null, location: bcryptNode.loc })); } } export default { name: "isUnsafePrehash", nodeTypes: [ "CallExpression", "FunctionDeclaration", "FunctionExpression", "ArrowFunctionExpression" ], validateNode, main: { default: bcryptHashCall, markAmbiguousParams }, initialize, breakOnMatch: false, context: {} }; //# sourceMappingURL=isUnsafePrehash.js.map