Command Injection

Command and argument injection risks in C# Process APIs and safer process execution.

Description

Untrusted input passed to Process.Start or ProcessStartInfo can introduce two kinds of risk.

  • Control of ProcessStartInfo.FileName or the first argument of Process.Start can let an attacker choose an unintended executable.
  • Input in ProcessStartInfo.Arguments or the second argument of Process.Start(string, string) can alter argument boundaries or options in the raw command line. If the executable is an interpreter such as cmd.exe, PowerShell, /bin/sh, or /bin/bash, input may be executed as command syntax.

UseShellExecute = false disables the operating system's document-association shell. It does not prevent the application from directly launching an interpreter such as cmd.exe, PowerShell, or sh.

ProcessStartInfo.ArgumentList and .NET 10's Process.Start(string, IEnumerable<string>) overload preserve argument boundaries, making them preferable to raw Arguments strings. The target program may still interpret an argument as an option, and values following sh -c, cmd /c, or PowerShell -Command remain executable code.

Potential impact

  • Arbitrary command or program execution with the application's privileges
  • File access, modification, or deletion; credential theft; and internal network access
  • Data exfiltration, privilege misuse, or service disruption through attacker-selected options

Remediation

  1. Prefer managed library APIs for files, compression, networking, or data conversion instead of starting processes.
  2. If a process is necessary, do not use external input as its executable path. Map validated operation identifiers to fixed absolute paths managed by the developer. Keep trusted executables in locations unprivileged users cannot modify.
  3. Set UseShellExecute to false and avoid command interpreters. Do not pass untrusted values after -c, /c, -Command, or similar code-execution options.
  4. Pass arguments individually with ProcessStartInfo.ArgumentList or the IEnumerable<string> overload. Apply allow-list validation and length limits appropriate to each value's syntax and meaning in the target program.
  5. If the target program supports it, place an end-of-options marker such as -- before untrusted operands. The marker works only when the program recognizes that convention.
  6. Run the process with minimal privileges and only necessary filesystem and network access. Do not assume one command-line escaping function works for every program.

Examples

Before

csharp
using Microsoft.AspNetCore.Mvc;
using System.Diagnostics;

public sealed class RunController : Controller
{
    public void Run([FromQuery] string command)
    {
        var processInfo = new ProcessStartInfo
        {
            FileName = "/bin/sh",
            UseShellExecute = false
        };
        processInfo.ArgumentList.Add("-c");
        processInfo.ArgumentList.Add(command);
        Process.Start(processInfo);
    }
}

Although ArgumentList preserves argument boundaries, the value after -c is code interpreted by the shell. UseShellExecute = false does not disable interpretation by the explicitly launched /bin/sh.

After

csharp
using System.Diagnostics;

static class Printer
{
    public static void PrintUserValue(string userInput)
    {
        var processInfo = new ProcessStartInfo
        {
            FileName = "/usr/bin/printf",
            UseShellExecute = false
        };
        processInfo.ArgumentList.Add("%s\n");
        processInfo.ArgumentList.Add(userInput);
        Process.Start(processInfo);
    }
}

This example selects a fixed executable and format string without using a command interpreter. The user value is a separate data argument. The deployment must ensure /usr/bin/printf exists and has trusted ownership. For simple output, an API such as Console.WriteLine avoids process creation entirely.

References