Command Injection

ID

swift.command_injection

Severity

critical

Resource

Injection

Language

Swift

Tags

CWE:77, CWE:78, NIST.SP.800-53, OWASP:2021:A3, PCI-DSS:6.5.1

Description

Improper neutralization of special elements used in a command ('Command Injection').

Command injection vulnerabilities occur when an application passes untrusted input to a system shell command without proper validation or sanitization.

Such vulnerabilities allow attackers to execute arbitrary shell commands with the privileges of the user running the application, which may result in complete system compromise.

Attackers exploiting the vulnerability can then install a reverse shell, download and install malware or ransomware, cryptocurrency miners, run database clients for data exfiltration, etc.

Understanding and mitigating this risk is crucial, as it can facilitate data breaches, unauthorized data manipulation, or any type of attack that could be crafted via system commands.

Rationale

OS command injection occurs in Swift when user-controlled input is passed to shell commands via the Process class without proper sanitization. When using shell interpreters like /bin/sh with the -c flag, attackers can inject shell metacharacters (;, |, &&) to execute arbitrary commands alongside the intended operation.

A rather trivial example of vulnerable code look like this:

import Foundation

func executeUserCommand(request: URLRequest) throws {
    guard let url = request.url,
          let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
          let queryItems = components.queryItems,
          let command = queryItems.first(where: { $0.name == "cmd" })?.value else {
        throw CommandError.invalidRequest
    }

    let process = Process()
    process.launchPath = "/bin/sh"

    // FLAW: User input directly passed to shell command
    process.arguments = ["-c", command]

    try process.run()
    process.waitUntilExit()
}

In this instance, the command parameter from the URL query is directly passed to the shell via Process arguments. An attacker could provide a value like ls; rm -rf / to execute arbitrary commands on the system.

Remediation

To safeguard against command injection, it is essential to adopt a series of preventive measures:

  1. Avoid Direct Command Execution with Untrusted Input: Avoid using functions for executing shell commands with untrusted inputs. Where command execution is necessary, parameterize inputs as separate command-line arguments, and do not concatenate untrusted inputs into shell commands.

  2. Input Validation and Whitelisting: Perform rigorous input validation to ensure that the input conforms to expected and safe formats. Whitelisting valid input patterns is preferable over blacklisting potentially harmful inputs.

  3. Escape Shell Metacharacters: If the input must be included in a command, ensure any shell metacharacters are properly blacklisted, escaped or sanitized using a dedicated library.

    Characters in { } ( ) < > & * ‘ | = ? ; [ ] ^ $ – # ~ ! . ” % / \ : + , \` are shell metacharacters for most OSes and shells.

    This is difficult to do well, and attackers have many ways of bypassing them so it is not recommended. You have been warned !

By implementing these practices, you can significantly minimize the potential for command injection vulnerabilities, enhancing the application’s resistance to this type of attack.

Here is the revised, secure code example using parameterized commands:

import Foundation

func executeUserCommand(request: URLRequest) throws {
    guard let url = request.url,
          let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
          let queryItems = components.queryItems,
          let filename = queryItems.first(where: { $0.name == "file" })?.value else {
        throw CommandError.invalidRequest
    }

    // FIXED: Use array-based arguments without shell interpretation
    let process = Process()
    process.executableURL = URL(fileURLWithPath: "/usr/bin/ls")

    // Validate and whitelist the filename
    let sanitized = filename.replacingOccurrences(of: "..", with: "")
                            .replacingOccurrences(of: "/", with: "")

    // Pass as individual arguments (no shell interpretation)
    process.arguments = ["-la", sanitized]

    try process.run()
    process.waitUntilExit()
}

Better approach - use whitelist validation:

import Foundation

enum AllowedCommand: String {
    case listFiles = "ls"
    case diskUsage = "du"
    case whoami = "whoami"

    var executablePath: String {
        switch self {
        case .listFiles: return "/usr/bin/ls"
        case .diskUsage: return "/usr/bin/du"
        case .whoami: return "/usr/bin/whoami"
        }
    }
}

func executeUserCommand(request: URLRequest) throws {
    guard let url = request.url,
          let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
          let queryItems = components.queryItems,
          let cmdString = queryItems.first(where: { $0.name == "cmd" })?.value,
          let allowedCmd = AllowedCommand(rawValue: cmdString) else {
        throw CommandError.invalidRequest
    }

    // FIXED: Whitelist of allowed commands
    let process = Process()
    process.executableURL = URL(fileURLWithPath: allowedCmd.executablePath)
    process.arguments = ["-la"] // Fixed, safe arguments only

    try process.run()
    process.waitUntilExit()
}

Alternative - avoid shell execution altogether:

import Foundation

func listUserDirectory(request: URLRequest) throws -> [String] {
    guard let url = request.url,
          let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
          let queryItems = components.queryItems,
          let dirName = queryItems.first(where: { $0.name == "dir" })?.value else {
        throw CommandError.invalidRequest
    }

    // FIXED: Use FileManager API instead of shell commands
    let fileManager = FileManager.default

    // Validate the directory path
    let sanitized = (dirName as NSString).lastPathComponent
    let baseURL = URL(fileURLWithPath: "/var/app/data/")
    let dirURL = baseURL.appendingPathComponent(sanitized)

    // Ensure within allowed directory
    guard dirURL.standardized.path.hasPrefix(baseURL.path) else {
        throw CommandError.invalidPath
    }

    // Use native API - no shell involved
    return try fileManager.contentsOfDirectory(atPath: dirURL.path)
}

Configuration

The detector has the following configurable parameters:

  • sources, that indicates the source kinds to check.

  • neutralizations, that indicates the neutralization kinds to check.

Unless you need to change the default behavior, you typically do not need to configure this detector.

References