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:
parent
68a9b16c2b
commit
da1c5b26e0
@ -29,6 +29,24 @@ const DEFAULT_TIMEOUT = 5 * 60 * 1000;
|
|||||||
/** デフォルトtmuxターゲット (MacBook用) */
|
/** デフォルトtmuxターゲット (MacBook用) */
|
||||||
const DEFAULT_TMUX_TARGET = "%2"; // カエデ (CodeGen) のペイン
|
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コマンドを実行
|
* tmuxコマンドを実行
|
||||||
*
|
*
|
||||||
@ -44,8 +62,9 @@ async function execTmux(
|
|||||||
): Promise<TmuxResult> {
|
): Promise<TmuxResult> {
|
||||||
const { timeout = DEFAULT_TIMEOUT, env = {} } = options;
|
const { timeout = DEFAULT_TIMEOUT, env = {} } = options;
|
||||||
|
|
||||||
// tmux send-keys でコマンドを送信
|
// tmux send-keys でコマンドを送信 (コマンドインジェクション対策)
|
||||||
const sendCommand = `tmux send-keys -t ${target} "${command}" Enter`;
|
const escapedCommand = escapeShellString(command);
|
||||||
|
const sendCommand = `tmux send-keys -t ${target} "${escapedCommand}" Enter`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// タイムアウト付きで実行
|
// タイムアウト付きで実行
|
||||||
@ -138,8 +157,9 @@ function buildCodexCommand(content: string, options: ReviewOptions): string {
|
|||||||
|
|
||||||
if (useFile) {
|
if (useFile) {
|
||||||
// TODO: 一時ファイルを使用する場合
|
// 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");
|
opts.push("--verbose");
|
||||||
}
|
}
|
||||||
|
|
||||||
const cmd = `codex review ${opts.join(" ")} ${content}`;
|
// コマンドインジェクション対策: contentをエスケープしてクォート
|
||||||
|
const escapedContent = escapeShellString(content);
|
||||||
|
const cmd = `codex review ${opts.join(" ")} "${escapedContent}"`;
|
||||||
return cmd;
|
return cmd;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -31,10 +31,12 @@ interface AgentDefinition {
|
|||||||
|
|
||||||
/** 利用可能なエージェント一覧 */
|
/** 利用可能なエージェント一覧 */
|
||||||
const AVAILABLE_AGENTS: AgentDefinition[] = [
|
const AVAILABLE_AGENTS: AgentDefinition[] = [
|
||||||
{ id: "conductor", intents: ["*"], capacity: 10 },
|
// 具体的な意図を持つエージェントを先に配置
|
||||||
{ id: "codegen", intents: ["create", "update"], capacity: 5 },
|
{ id: "codegen", intents: ["create", "update"], capacity: 5 },
|
||||||
{ id: "review", intents: ["verify", "check"], capacity: 3 },
|
{ id: "review", intents: ["verify", "check"], capacity: 3 },
|
||||||
{ id: "deploy", intents: ["deploy", "release"], capacity: 2 },
|
{ 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 {
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user