@nodesecure/js-x-ray
Version:
JavaScript AST XRay analysis
122 lines • 4.28 kB
JavaScript
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