Skip to content
49 changes: 45 additions & 4 deletions .agents/plugins/opencode-aidevops/tools.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,18 @@
};
}

/**
* Validate that a CLI command string contains only safe characters.
* Allows alphanumeric, spaces, hyphens, underscores, dots, forward slashes,
* colons, hash signs (#), and at-signs (@) — sufficient for all aidevops subcommands and file path arguments.
* Rejects shell metacharacters ($, `, ;, |, &, (, ), etc.).
* @param {string} command
* @returns {boolean}
*/
function isSafeCommand(command) {
return /^[a-zA-Z0-9 _\-./:#@]+$/.test(command);
}

/**
* Create the aidevops CLI tool.
* @param {function} run - Shell command runner
Expand All @@ -51,7 +63,11 @@
description:
'Run aidevops CLI commands (status, repos, features, secret, etc.). Pass command as string e.g. "status", "repos", "features"',
async execute(args) {
const cmd = `aidevops ${args.command || args}`;
const rawCmd = String(args.command || args);
if (!isSafeCommand(rawCmd)) {
return `Error: command contains disallowed characters. Only alphanumeric, spaces, hyphens, underscores, dots, slashes, colons, # and @ are permitted.`;
}
const cmd = `aidevops ${rawCmd}`;
const result = run(cmd, 15000);
return result || `Command completed: ${cmd}`;
},
Expand Down Expand Up @@ -79,7 +95,7 @@
return "pre-edit-check.sh not found — cannot verify git safety";
}
const taskFlag = args.task
? ` --loop-mode --task "${args.task}"`
? ` --loop-mode --task ${shellEscape(args.task)}`
: "";
try {
const result = execSync(`bash "${script}"${taskFlag}`, {
Expand Down Expand Up @@ -171,16 +187,41 @@
};
}

/**
* Allowlist map for install-hooks-helper.sh actions.
* Object.create(null) eliminates the prototype chain, preventing inherited
* properties (toString, constructor, __proto__) from bypassing validation.
* Object-literal lookup severs the taint chain — the value returned is from
* this constant, not derived from the caller's input.
*/
const HOOK_ACTION_MAP = Object.freeze(Object.assign(Object.create(null), {
install: "install",
uninstall: "uninstall",
status: "status",
test: "test",
}));

/**
* Run the install-hooks-helper.sh script.
* @param {string} helperScript
* @param {string} action
* @returns {string}
*/
function runHookHelper(helperScript, action) {
// Own-property check + lookup returns a string owned by HOOK_ACTION_MAP,
// completely severing the taint chain from the function parameter.
// Object.hasOwn guards against any inherited properties even if
// Object.create(null) is accidentally reverted.
const actionKey = String(action);
const validAction = Object.hasOwn(HOOK_ACTION_MAP, actionKey)
? HOOK_ACTION_MAP[actionKey]
: undefined;
if (!validAction) {
return `Invalid action: ${String(action)}. Valid actions: ${Object.keys(HOOK_ACTION_MAP).join(", ")}`;
}
try {
const result = execSync(
`bash "${helperScript}" ${action}`,
`bash "${helperScript}" ${validAction}`,

Check failure on line 224 in .agents/plugins/opencode-aidevops/tools.mjs

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

.agents/plugins/opencode-aidevops/tools.mjs#L224

Detected calls to child_process from a function argument `action`. This could lead to a command injection if the input is user controllable.
{
encoding: "utf-8",
timeout: 15000,
Expand All @@ -190,7 +231,7 @@
return result.trim();
} catch (err) {
const cmdOutput = (err.stdout || "") + (err.stderr || "");
return `Hook ${action} failed:\n${cmdOutput.trim()}`;
return `Hook ${validAction} failed:\n${cmdOutput.trim()}`;
}
}

Expand Down