@nodesecure/js-x-ray
Version:
JavaScript AST XRay analysis
110 lines • 3.88 kB
JavaScript
// Import Internal Dependencies
import { generateWarning } from "../warnings.js";
import { isLiteral, isTemplateLiteral } from "../types/estree.js";
// CONSTANTS
const kUnsafeCommands = ["csrutil", "uname", "ping", "curl"];
function isUnsafeCommand(command) {
return kUnsafeCommands.some((unsafeCommand) => command.includes(unsafeCommand));
}
function isSpawnOrExec(name) {
return name === "spawn" ||
name === "exec" ||
name === "spawnSync" ||
name === "execSync";
}
function getCommand(commandArg) {
let command = "";
switch (commandArg.type) {
case "Literal":
command = commandArg.value;
break;
case "TemplateLiteral":
command = commandArg.quasis.at(0)?.value.raw;
break;
}
return command;
}
/**
* @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) {
if (node.type !== "CallExpression" || node.arguments.length === 0) {
return [false];
}
// const { spawn } = require("child_process");
// spawn("...", ["..."]);
// or
// const { exec } = require("child_process");
// exec(...);
if (node.type === "CallExpression" &&
node.callee.type === "Identifier" &&
isSpawnOrExec(node.callee.name)) {
return [true, node.callee.name];
}
// child_process.spawn(...) or require("child_process").spawn(...)
// child_process.exec(...) or require("child_process").exec(...)
if (node.callee.type === "MemberExpression" &&
node.callee.property.type === "Identifier" &&
isSpawnOrExec(node.callee.property.name)) {
// child_process.spawn(...)
// child_process.exec(...)
if (node.callee.object.type === "Identifier" &&
node.callee.object.name === "child_process") {
return [true, node.callee.property.name];
}
// require("child_process").spawn(...)
// require("child_process").exec(...)
if (node.callee.object.type === "CallExpression" &&
node.callee.object.callee.type === "Identifier" &&
node.callee.object.callee.name === "require" &&
node.callee.object.arguments.length === 1 &&
node.callee.object.arguments[0].type === "Literal" &&
node.callee.object.arguments[0].value === "child_process") {
return [true, node.callee.property.name];
}
}
return [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);
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") {
const arrExpr = node.arguments.at(1);
if (arrExpr && arrExpr.type === "ArrayExpression") {
arrExpr.elements
.filter((element) => isLiteral(element))
.forEach((element) => {
command += ` ${element.value}`;
});
}
}
const warning = generateWarning("unsafe-command", {
value: command,
location: node.loc
});
sourceFile.warnings.push(warning);
return signals.Skip;
}
return null;
}
export default {
name: "isUnsafeCommand",
validateNode,
main
};
//# sourceMappingURL=isUnsafeCommand.js.map