fix(security): fix P0 critical bugs in agent selection and command injection

P0-1: Fix Agent Selection Bug (decide.ts)
- Move wildcard agent (conductor) to end of AVAILABLE_AGENTS array
- Update selectAgent() to prioritize specific intents over wildcard
- Previously, wildcard matched first, causing all intents to select conductor

P0-2: Fix Command Injection Vulnerability (codex-reviewer.ts)
- Add escapeShellString() function to sanitize user content
- Escape backslashes, quotes, $, backticks, newlines, carriage returns
- Apply escaping in execTmux() and buildCodexCommand()
- Previously, unescaped user content could execute arbitrary shell commands

Security: Prevents attackers from injecting malicious commands via
user-controlled content (code review input) that gets passed to tmux.
This commit is contained in:
Shunsuke Hayashi 2026-01-26 07:25:36 +09:00
parent 68a9b16c2b
commit da1c5b26e0
2 changed files with 43 additions and 8 deletions

View File

@ -29,6 +29,24 @@ const DEFAULT_TIMEOUT = 5 * 60 * 1000;
/** デフォルトtmuxターゲット (MacBook用) */
const DEFAULT_TMUX_TARGET = "%2"; // カエデ (CodeGen) のペイン
/**
*
* tmux send-keys
*
* @param str -
* @returns
*/
function escapeShellString(str: string): string {
// シェル特殊文字をエスケープ
return str
.replace(/\\/g, "\\\\") // バックスラッシュ
.replace(/"/g, '\\"') // ダブルクォート
.replace(/\$/g, "\\$") // ドル記号
.replace(/`/g, "\\`") // バッククォート
.replace(/\n/g, "\\n") // 改行
.replace(/\r/g, "\\r"); // キャリッジリターン
}
/**
* tmuxコマンドを実行
*
@ -44,8 +62,9 @@ async function execTmux(
): Promise<TmuxResult> {
const { timeout = DEFAULT_TIMEOUT, env = {} } = options;
// tmux send-keys でコマンドを送信
const sendCommand = `tmux send-keys -t ${target} "${command}" Enter`;
// tmux send-keys でコマンドを送信 (コマンドインジェクション対策)
const escapedCommand = escapeShellString(command);
const sendCommand = `tmux send-keys -t ${target} "${escapedCommand}" Enter`;
try {
// タイムアウト付きで実行
@ -138,8 +157,9 @@ function buildCodexCommand(content: string, options: ReviewOptions): string {
if (useFile) {
// TODO: 一時ファイルを使用する場合
// 今回は簡易的に引数渡し
return `codex review "${content.replace(/"/g, '\\"')}"`;
// 今回は簡易的に引数渡し (コマンドインジェクション対策)
const escapedContent = escapeShellString(content);
return `codex review "${escapedContent}"`;
}
// オプションを付与
@ -157,7 +177,9 @@ function buildCodexCommand(content: string, options: ReviewOptions): string {
opts.push("--verbose");
}
const cmd = `codex review ${opts.join(" ")} ${content}`;
// コマンドインジェクション対策: contentをエスケープしてクォート
const escapedContent = escapeShellString(content);
const cmd = `codex review ${opts.join(" ")} "${escapedContent}"`;
return cmd;
}

View File

@ -31,10 +31,12 @@ interface AgentDefinition {
/** 利用可能なエージェント一覧 */
const AVAILABLE_AGENTS: AgentDefinition[] = [
{ id: "conductor", intents: ["*"], capacity: 10 },
// 具体的な意図を持つエージェントを先に配置
{ id: "codegen", intents: ["create", "update"], capacity: 5 },
{ id: "review", intents: ["verify", "check"], capacity: 3 },
{ id: "deploy", intents: ["deploy", "release"], capacity: 2 },
// ワイルドカードエージェントは最後に配置 (fallbackとして機能)
{ id: "conductor", intents: ["*"], capacity: 10 },
];
/**
@ -179,10 +181,21 @@ function selectStrategy(intent: string): string {
/**
*
*
* fallbackとして使用
*/
function selectAgent(intent: string): string | undefined {
const agent = AVAILABLE_AGENTS.find((a) => a.intents.includes(intent) || a.intents.includes("*"));
return agent?.id;
// まず具体的な意図を探す
const specificAgent = AVAILABLE_AGENTS.find(
(a) => a.intents.includes(intent) && !a.intents.includes("*"),
);
if (specificAgent) {
return specificAgent.id;
}
// 具体的なマッチがなければワイルドカードを探す
const wildcardAgent = AVAILABLE_AGENTS.find((a) => a.intents.includes("*"));
return wildcardAgent?.id;
}
/**