@nodesecure/js-x-ray
Version:
JavaScript AST XRay analysis
112 lines • 3.64 kB
JavaScript
// Import Internal Dependencies
import { toLiteral } from "../estree/index.js";
import { CALL_EXPRESSION_DATA } from "../contants.js";
import { isLiteral, isTemplateLiteral } from "../estree/types.js";
import { generateWarning } from "../warnings.js";
// CONSTANTS
const kUnsafeCommands = ["csrutil", "uname", "ping", "curl"];
// CONSTANTS
const kIdentifierOrMemberExps = [
"child_process.spawn",
"child_process.spawnSync",
"child_process.exec",
"child_process.execSync"
];
function isUnsafeCommand(command) {
return kUnsafeCommands.some((unsafeCommand) => command.includes(unsafeCommand));
}
function getCommand(commandArg) {
let command = "";
switch (commandArg.type) {
case "Literal":
command = commandArg.value;
break;
case "TemplateLiteral":
command = toLiteral(commandArg);
break;
}
return command;
}
function concatArrayArgs(command, node) {
const arrExpr = node.arguments.at(1);
let finalizedCommand = command;
if (arrExpr && arrExpr.type === "ArrayExpression") {
arrExpr.elements
.filter((element) => isLiteral(element))
.forEach((element) => {
finalizedCommand += ` ${element.value}`;
});
}
return finalizedCommand;
}
/**
* @description Detect spawn or exec unsafe commands
* @example
* child_process.spawn("csrutil", ["status"]);
*
* require("child_process").spawn("csrutil", ["disable"]);
*
* const { exec } = require("child_process");
* exec("csrutil status");
*/
function validateNode(_node, ctx) {
const data = ctx.context?.[CALL_EXPRESSION_DATA];
return data && kIdentifierOrMemberExps.includes(data.name) ? [
true,
data.name.slice("child_process.".length)
] : [false];
}
function main(node, ctx) {
const { sourceFile, data: methodName, signals } = ctx;
const commandArg = node.arguments[0];
if (!isLiteral(commandArg) && !isTemplateLiteral(commandArg)) {
return null;
}
let command = getCommand(commandArg);
// Aggressive mode: warn on any child_process usage
if (sourceFile.sensitivity === "aggressive") {
// Handle spawn/spawnSync array arguments
if (methodName === "spawn" || methodName === "spawnSync") {
command = concatArrayArgs(command, node);
}
const warning = generateWarning("unsafe-command", {
value: command,
location: node.loc
});
sourceFile.warnings.push(warning);
return signals.Skip;
}
// Conservative mode: existing strict validation
if (isUnsafeCommand(command)) {
// Spawned command arguments are filled into an Array
// as second arguments. This is why we should add them
// manually to the command string.
if (methodName === "spawn" || methodName === "spawnSync") {
command = concatArrayArgs(command, node);
}
const warning = generateWarning("unsafe-command", {
value: command,
location: node.loc
});
sourceFile.warnings.push(warning);
return signals.Skip;
}
return null;
}
function initialize(ctx) {
kIdentifierOrMemberExps.forEach((identifierOrMemberExp) => {
const moduleName = identifierOrMemberExp.split(".")[0];
ctx.sourceFile.tracer.trace(identifierOrMemberExp, {
followConsecutiveAssignment: true,
moduleName
});
});
}
export default {
name: "isUnsafeCommand",
validateNode,
main,
initialize,
context: {}
};
//# sourceMappingURL=isUnsafeCommand.js.map