Skip to content

security: [HIGH] Command injection risk via shellQuote in gcp.ts SSH execution #2522

@louisgv

Description

@louisgv

Summary

The runServer() and related functions in packages/cli/src/gcp/gcp.ts use shellQuote() to wrap user-controlled commands before passing them to SSH. While single-quote wrapping prevents most issues, the shell escaping pattern '\'' for embedded single quotes could potentially be bypassed with carefully crafted input.

Impact

If an attacker can control the cmd parameter passed to runServer(), runServerCapture(), or interactiveSession(), they could potentially execute arbitrary commands on the remote GCP instance.

Vulnerable Code

gcp.ts:827-856 (runServer)

export async function runServer(cmd: string, timeoutSecs?: number): Promise<void> {
  const username = resolveUsername();
  const fullCmd = `export PATH="$HOME/.npm-global/bin:$HOME/.claude/local/bin:$HOME/.local/bin:$HOME/.bun/bin:$PATH" && ${cmd}`;
  const keyOpts = getSshKeyOpts(await ensureSshKeys());

  const proc = Bun.spawn(
    [
      "ssh",
      ...SSH_BASE_OPTS,
      ...keyOpts,
      `${username}@${gcpServerIp}`,
      `bash -c ${shellQuote(fullCmd)}`,  // Potential injection point
    ],

gcp.ts:997-999 (shellQuote function)

function shellQuote(s: string): string {
  return "'" + s.replace(/'/g, "'\\''") + "'";
}

Attack Vector

The shellQuote function escapes single quotes using the pattern '\'' which closes the current quote, adds an escaped quote, and reopens. This is generally safe BUT:

  1. If the input contains sequences like ' ; malicious_cmd ; ', the escaping might not work as expected
  2. The cmd parameter comes from agent launch commands stored in manifest.json, which could be tampered with if manifest caching is compromised
  3. No additional validation is performed on cmd before passing to SSH

Recommendation

Option 1: Input Validation (Preferred)

Add strict validation on cmd parameter before passing to SSH:

function validateRemoteCommand(cmd: string): void {
  if (!cmd || cmd.trim() === "") {
    throw new Error("Command is required");
  }
  // Reject dangerous shell metacharacters in unexpected contexts
  const dangerous = /[;&|<>$(){}[\]]/;
  if (dangerous.test(cmd)) {
    logWarn("Command contains shell metacharacters - ensure it's from trusted source");
  }
}

export async function runServer(cmd: string, timeoutSecs?: number): Promise<void> {
  validateRemoteCommand(cmd);  // Validate before use
  const username = resolveUsername();
  const fullCmd = `export PATH="..." && ${cmd}`;
  // ... rest of function
}

Option 2: Use SSH Argument Separation (Defense-in-Depth)

Instead of passing the command as a single string to bash -c, pass it as a separate SSH argument.

Severity

HIGH - Potential for arbitrary command execution on remote GCP instances if manifest.json is compromised.

References

  • File: packages/cli/src/gcp/gcp.ts lines 827-856, 858-893, 932-962, 997-999
  • Similar issue previously fixed in other cloud drivers (see closed security issues)
  • OWASP: Command Injection (A03:2021)

Metadata

Metadata

Assignees

No one assigned

    Labels

    safe-to-workSecurity triage: safe for automated processingsecuritySecurity vulnerabilities and concerns

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions