Root cause
Injection & unsafe execution introduced
smart_user getUserDetails interpolates the caller-supplied username into a getent/grep shell string (commit fc358254).
How AI contributed: Direct introductionPublished Aug 14, 2026 in
ooples/token-optimizer-mcpAll 2 findings in this repository
CWE-78 OS command injection in smart_user get-user-info: caller-controlled username is interpolated into a POSIX shell string executed via child_process exec/execAsync (getent passwd "${username}" || grep "^${username}:" /etc/passwd). Double quotes do not prevent $(...) / backtick command substitution, so an MCP client can run arbitrary commands as the server user.
The smart_user get-user-info operation builds the Unix user lookup as a getent and grep command string and interpolates the caller-supplied username into it before running that string through execAsync. Double quotes do not stop command substitution, so a crafted username reaches the shell. The fix passes the username to getent as a separate argument through execFileSafe after assertSafeArg validation.
Root cause
smart_user getUserDetails interpolates the caller-supplied username into a getent/grep shell string (commit fc358254).
Fix
The fix removes the shell-interpolated getent and grep pipeline, invokes getent with the username as a separate argv value through execFileSafe, and performs the passwd-file fallback in process.
Case facts
@@ -912,11 +912,14 @@ } } else { // Unix user details try {+ const { stdout: passwdOut } = await execAsync(+ `getent passwd "${username}" || grep "^${username}:" /etc/passwd`,+ ); const parts = passwdOut.trim().split(":"); if (parts.length < 7) { throw new Error(`Invalid passwd entry for user: ${username}`); } } } }@@ -706,8 +713,83 @@ export class SmartUser { // =========================== // Platform-Specific Methods // =========================== + /**+ * Read the full passwd database. Tries `getent passwd` (argv mode) and falls+ * back to reading /etc/passwd directly when getent is unavailable. Replaces+ * the former `getent passwd || cat /etc/passwd` shell pipeline.+ */+ private async readPasswdDatabase(): Promise<string> {+ try {+ const { stdout } = await execFileSafe('getent', ['passwd']);+ if (stdout.trim()) return stdout;+ } catch {+ // getent not available; fall through to the file read.+ }+ return readFileSync('/etc/passwd', 'utf-8');+ }++ /**+ * Read the full group database. Tries `getent group` (argv mode) and falls+ * back to /etc/group. Replaces the former `getent group || cat /etc/group`.+ */+ private async readGroupDatabase(): Promise<string> {+ try {+ const { stdout } = await execFileSafe('getent', ['group']);+ if (stdout.trim()) return stdout;+ } catch {+ // getent not available; fall through to the file read.+ }+ return readFileSync('/etc/group', 'utf-8');+ }++ /**+ * Look up a single passwd entry by username. Tries `getent passwd <user>`+ * (argv mode) and falls back to scanning /etc/passwd in-process. Replaces the+ * injectable `getent passwd "${username}" || grep "^${username}:" ...`.+ */+ private async lookupPasswdEntry(username: string): Promise<string> {+ assertSafeArg(username, 'username');+ try {+ const { stdout } = await execFileSafe('getent', ['passwd', username]);+ if (stdout.trim()) return stdout.trim();+ } catch {+ // fall through to file scan+ }+ const contents = readFileSync('/etc/passwd', 'utf-8');+ const match = contents+ .split('\n')+ .find((line) => line.startsWith(`${username}:`));+ if (!match) {+ throw new Error(`User not found: ${username}`);+ }+ return match;+ }++ /**+ * Look up a single group entry by name. Tries `getent group <name>` (argv+ * mode) and falls back to scanning /etc/group. Replaces the injectable+ * `getent group "${groupname}" || grep "^${groupname}:" ...`.+ */+ private async lookupGroupEntry(groupname: string): Promise<string> {AI-assisted change 0e12e71a62329d388a584586f82d3291f94b09a32328b025588c656d213d970f · Fix 68ecbe04a8399a33f2781fe570b880693fef731352dd383e489897847af0938b