Root cause
Injection & unsafe execution introduced
Complete taxonomy refactor wish 424
How AI contributed: Direct introductionPublished May 11, 2026 in
automagik-dev/genie
OS command injection (CWE-78) in automagik-genie MCP server: readTranscriptFromCommit() in src/mcp/server.ts builds git --git-dir="${gitDir}" log -1 --format="%B" ${commitSha} and runs it via child_process.execSync with string interpolation. commitSha (from Forge API after_head_commit) is interpolated outside quotes (unrestricted shell injection) and gitDir (from container_ref) inside quotes (still evaluates $()/backticks). Second-order injection: attacker controlling FORGE_BASE_URL or poisoning Forge API responses can achieve RCE when a user views a task (view_task/view tool).
Root cause
Complete taxonomy refactor wish 424
Fix status
No minimum fix commit has been established for this finding.
Fix status unresolved
Case facts
@@ -22,6 +22,7 @@ import { z } from 'zod'; import fs from 'fs'; import path from 'path'; import WebSocket from 'ws';+import { execSync } from 'child_process'; import { transformDisplayPath } from './lib/display-transform.js'; // Import WebSocket-native tools (MVP Phase 6)@@ -66,6 +67,10 @@ import { // Import neuron resource provider import { initNeuronProvider, getNeuronProvider } from './resources/neuron-provider.js'; import { getMcpConfig, getForgeConfig } from './lib/service-config.js';+import { resolveAgentIdentifier, loadAgentSpec } from '../cli/lib/agent-resolver.js';+import { normalizeExecutorKeyOrDefault } from '../cli/lib/executor-registry.js';+import { monitorTaskCompletion } from '../cli/lib/task-monitor.js';+import { TaskService } from '../cli/cli-core/task-service.js'; const { port: PORT } = getMcpConfig(); const TRANSPORT = process.env.MCP_TRANSPORT || 'stdio';@@ -127,12 +132,24 @@ async function viewSession(taskId: string): Promise<{status: string; transcript: }; } - let transcript = await streamNormalizedLogs(forge, latestProcess.id, token);+ const completedStatuses = new Set(['completed', 'success', 'failed', 'error', 'stopped']);+ if (completedStatuses.has(status)) {+ const gitTranscript = readTranscriptFromCommit(attemptDetails, latestProcess);+ if (gitTranscript) {+ return { status, transcript: gitTranscript };+ }+ }++ let transcript = await streamNormalizedLogs(forge, latestProcess.id, token, 10000); if (!transcript) { transcript = latestProcess.output || latestProcess.logs || null; } + if (!transcript && completedStatuses.has(status)) {+ transcript = readTranscriptFromCommit(attemptDetails, latestProcess);+ }+ return { status, transcript }; } catch (error: any) { return {@@ -143,6 +160,33 @@ async function viewSession(taskId: string): Promise<{status: string; transcript: } } +function readTranscriptFromCommit(attemptDetails: any, latestProcess: any): string | null {+ const commitSha = latestProcess?.after_head_commit || latestProcess?.afterHeadCommit;+ const containerRef = attemptDetails?.container_ref || attemptDetails?.containerRef;++ if (!commitSha || !containerRef) {+ return null;+ }++ const gitDir = path.join(containerRef, '.git');+ if (!fs.existsSync(gitDir)) {+ return null;+ }++ try {+ const output = execSync(`git --git-dir="${gitDir}" log -1 --format="%B" ${commitSha}`, {+ encoding: 'utf8',+ stdio: ['ignore', 'pipe', 'pipe']+ }).trim();+ if (!output.length) {+ return null;+ }+ return `Commit ${commitSha.slice(0, 7)}\n${output}`;+ } catch {+ return null;+ }+}+ async function waitForLatestProcess( forge: any, attemptId: string,@@ -376,20 +420,22 @@ server.tool('list_tasks', 'List active and recent Genie agent tasks. Shows task const taskToolShape = { agent: z.string().describe('Agent ID to run (e.g., "plan", "implementor", "debug"). Get available agents from list_agents tool.'), prompt: z.string().describe('Detailed task description for the agent. Be specific about goals, context, and expected outcomes. Agents work best with clear, actionable prompts.'),- name: z.string().optional().describe('Friendly task name for easy identification (e.g., "bug-102-fix", "auth-feature"). If omitted, auto-generates: "{agent}-{timestamp}".')+ name: z.string().optional().describe('Friendly task name for easy identification (e.g., "bug-102-fix", "auth-feature"). If omitted, auto-generates: "{agent}-{timestamp}".'),+ executor: z.string().optional().describe('Override executor key (e.g., "OPENCODE"). Uses agent/default config when omitted.'),+ model: z.string().optional().describe('Override model for the selected executor.'),+ monitor: z.boolean().optional().describe('Set true to wait for completion with WebSocket streaming. Default: false (return immediately).') }; const TaskToolParams = z.object(taskToolShape);