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