Skip to content

[HIGH] Command injection risk in SSH connection parameter handling #1381

@louisgv

Description

@louisgv

Security Finding

Severity: HIGH
Component: CLI - Server connection
File: cli/src/commands.ts lines 1876-1895

Description

The cmdConnect() function constructs shell commands using unvalidated connection parameters (connection.ip, connection.user, connection.server_name) from user-controlled JSON history files. While these values are typically generated by spawn scripts, if the history file is manually edited or compromised, shell metacharacters could lead to command injection.

Additionally, display functions at lines 1439-1442, 1473-1480, and 1888 also use these unvalidated values when constructing command strings for display.

Vulnerable Code

// Line 1876-1883: Sprite console connection
if (connection.ip === "sprite-console" && connection.server_name) {
  p.log.step(`Connecting to sprite ${pc.bold(connection.server_name)}...`);
  return runInteractiveCommand(
    "sprite",
    ["console", "-s", connection.server_name],  // ❌ UNVALIDATED
    "Sprite console connection failed",
    `sprite console -s ${connection.server_name}`  // ❌ UNVALIDATED
  );
}

// Line 1886-1895: SSH connection  
p.log.step(`Connecting to ${pc.bold(connection.ip)}...`);
const sshCmd = `ssh -o StrictHostKeyChecking=accept-new ${connection.user}@${connection.ip}`;  // ❌ UNVALIDATED

return runInteractiveCommand(
  "ssh",
  ["-o", "StrictHostKeyChecking=accept-new", `${connection.user}@${connection.ip}`],  // ❌ UNVALIDATED
  "SSH connection failed",
  sshCmd
);

Attack Vector

  1. User's ~/.spawn/history.json is manually edited or compromised:

    {
      "agent": "claude",
      "cloud": "hetzner", 
      "timestamp": "2026-02-17T12:00:00Z",
      "connection": {
        "ip": "1.2.3.4; malicious-command; echo ",
        "user": "root",
        "server_name": "test; rm -rf /tmp/pwned; echo "
      }
    }
  2. User runs spawn list and selects the corrupted record to connect

  3. The shell command is constructed with the malicious values embedded

  4. Command executes with user's privileges

Risk Assessment

  • Likelihood: Medium (requires history file compromise or manual editing)
  • Impact: High (arbitrary command execution with user privileges)
  • Overall: HIGH

Proof of Concept

# 1. Corrupt history file
cat > ~/.spawn/history.json <<'JSON'
[{
  "agent": "claude",
  "cloud": "hetzner",
  "timestamp": "2026-02-17T12:00:00Z",
  "connection": {
    "ip": "1.2.3.4",
    "user": "root\"; touch /tmp/pwned; echo \"pwned",
    "server_name": "test"
  }
}]
JSON

# 2. List history (triggers display functions with unvalidated values)
spawn list

# 3. Observe that unvalidated values are interpolated into command strings

Remediation

Add validation for connection parameters in history.ts and commands.ts:

1. Create validation functions in security.ts:

/**
 * Validates an IP address or special connection sentinel value.
 * Allows: IPv4, IPv6, and specific sentinel values (sprite-console, fly-ssh, daytona-sandbox)
 */
export function validateConnectionIP(ip: string): void {
  const sentinels = ["sprite-console", "fly-ssh", "daytona-sandbox"];
  if (sentinels.includes(ip)) return;
  
  // IPv4 or IPv6 validation
  const ipv4Pattern = /^(\d{1,3}\.){3}\d{1,3}$/;
  const ipv6Pattern = /^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}$/;
  
  if (!ipv4Pattern.test(ip) && !ipv6Pattern.test(ip)) {
    throw new Error(`Invalid connection IP: "${ip}"`);
  }
}

/**
 * Validates a Unix username.
 * Pattern: lowercase letters, digits, underscores, hyphens, optional $ suffix
 */
export function validateUsername(username: string): void {
  const usernamePattern = /^[a-z_][a-z0-9_-]*\$?$/;
  if (!usernamePattern.test(username)) {
    throw new Error(`Invalid username: "${username}"`);
  }
}

/**
 * Validates a server name/ID (cloud provider identifier).
 */
export function validateServerIdentifier(name: string): void {
  validateIdentifier(name, "Server identifier");
}

2. Validate in cmdConnect() before use:

async function cmdConnect(connection: VMConnection): Promise<void> {
  // SECURITY: Validate all connection parameters before use
  try {
    validateConnectionIP(connection.ip);
    validateUsername(connection.user);
    if (connection.server_name) {
      validateServerIdentifier(connection.server_name);
    }
  } catch (err) {
    p.log.error(`Security check failed: ${getErrorMessage(err)}`);
    p.log.error(`Your spawn history file may be corrupted or tampered with.`);
    p.log.info(`Location: ${getHistoryPath()}`);
    process.exit(1);
  }
  
  // ... rest of function unchanged ...
}

3. Add validation helper to history.ts:

import { validateConnectionIP, validateUsername, validateServerIdentifier } from "./security.js";

/** Validate connection data before saving to history */
function validateConnection(connection: VMConnection): void {
  validateConnectionIP(connection.ip);
  validateUsername(connection.user);
  if (connection.server_name) {
    validateServerIdentifier(connection.server_name);
  }
}

export function saveSpawnRecord(record: SpawnRecord): void {
  // Validate connection data if present
  if (record.connection) {
    try {
      validateConnection(record.connection);
    } catch (err) {
      // Log validation failure but don't block save (history is append-only)
      console.error(`Warning: Invalid connection data: ${getErrorMessage(err)}`);
    }
  }
  
  // ... rest of function unchanged ...
}

References


Reported by: security/code-scanner
Scan date: 2026-02-17

Metadata

Metadata

Assignees

No one assigned

    Labels

    in-progressIssue is being actively worked onsafe-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