Root cause
Injection & unsafe execution introduced
Initial commit: Yii2 MCP Server
How AI contributed: Direct introductionPublished May 2, 2026 in
artmin96/yii2-mcp-server
OS command injection (CWE-77) in the MCP tools yii_command_help and yii_execute_command: user-supplied command and args are interpolated raw into a shell command php "<yiiScript>" <command> <args...> executed via child_process exec() (shell=true) in src/yii2.ts; the only guard is a 3-entry denylist (migrate/fresh, migrate/down, cache/flush-all) using string .includes(), trivially bypassable (e.g. 'migrate/fresh; id'). getCommandHelp() has the same flaw (php "..." help ${command}). CVSS 6.3, remote (requires MCP session, PR:L); exploit published; project notified but did not respond.
src/yii2.ts builds a shell string such as php "<yiiScript>" <command> <args...> and passes it to child_process exec(), so MCP-supplied command and args values reach the shell unescaped. The only check is a three-entry denylist of destructive Yii commands matched with .includes(), which a value like migrate/fresh; id defeats because the extra command never appears in the list. getCommandHelp() interpolates its argument the same way.
Root cause
Initial commit: Yii2 MCP Server
Fix status
No minimum fix commit has been established for this finding.
Fix status unresolved
Case facts
+ }+ categories.get(category)!.push(cmd);+ });++ for (const [category, categoryCommands] of categories) {+ result += `### ${category}\n`;+ categoryCommands.forEach(cmd => {+ result += `• **${cmd.name}** - ${cmd.description}\n`;+ if (cmd.actions.length > 0) {+ cmd.actions.forEach(action => {+ result += ` - ${action}\n`;+ });+ }+ });+ result += '\n';+ }++ return {+ content: [+ {+ type: "text",+ text: result+ }+ ]+ };+ } catch (error) {+ throw new Error(`Failed to list commands: ${error instanceof Error ? error.message : String(error)}`);+ }+ }++ async getCommandHelp(command: string) {+ try {+ const { stdout } = await execAsync(`php "${this.yiiScript}" help ${command}`, {+ cwd: this.projectPath+ });++ return {+ content: [+ {+ type: "text",+ text: `Help for command: ${command}\n\n${stdout}`+ }+ ]+ };+ } catch (error) {+ throw new Error(`Failed to get help for ${command}: ${error instanceof Error ? error.message : String(error)}`);+ }+ }++ async executeCommand(command: string, args: string[] = [], interactive: boolean = false) {+ try {+ // Safety check - avoid dangerous commands+ const dangerousCommands = [+ 'migrate/fresh',+ 'migrate/down',+ 'cache/flush-all'+ ];+ + if (dangerousCommands.some(dangerous => command.includes(dangerous))) {+ return {+ content: [+ {+ type: "text",+ text: `Command '${command}' is considered dangerous and blocked for safety. Use interactive mode or run manually if needed.`+ }+ ]+ };+ }++ const fullCommand = `php "${this.yiiScript}" ${command} ${args.join(' ')}`.trim();+ + if (interactive) {+ return {+ content: [+ {+ type: "text",+ text: `Interactive command detected: ${fullCommand}\n\nPlease run this command manually in your terminal as it requires user input.`+ }+ ]+ };+ }++ const { stdout, stderr } = await execAsync(fullCommand, {+ cwd: this.projectPath,+ timeout: 30000 // 30 second timeout+ });++ let result = `Command executed: ${fullCommand}\n\n`;+ if (stdout) {+ result += `Output:\n${stdout}\n`;