CWE-78: OS Command Injection - C#
Overview
OS Command Injection occurs when an application incorporates untrusted data into an operating system command without proper validation or sanitization. Attackers can execute arbitrary commands on the host operating system.
Primary Defence: Use .NET native APIs (File, Directory, HttpClient, etc.) instead of system commands, or if unavoidable, use ProcessStartInfo.ArgumentList (.NET 6+) with UseShellExecute = false.
Remediation Strategy
PRIMARY FIX - Avoid System Calls
Use .NET native APIs instead of executing commands
- This eliminates the vulnerability entirely
- Do NOT use Process.Start() if a .NET API exists
SECONDARY FIX - Use ArgumentList Property
(.NET 6+) or properly escaped arguments with UseShellExecute = false
- WARNING: Only use if Priority 1 is not possible
- Must disable shell execution
Defense in Depth - Input Validation
Allowlist permitted characters
- Required in addition to Priority 1 or 2
- Never use validation alone
Additional Hardening - Least Privilege
Run as low-privilege user
- Apply alongside other fixes
Decision Tree
Need to execute OS command?
├─ Is there a .NET API alternative? (File, Directory, HttpClient, etc.)
│ ├─ YES → Use .NET API (Priority 1) - PREFERRED SOLUTION
│ └─ NO → Continue
│
├─ Can you use .NET 6+ ArgumentList?
│ ├─ YES → Use ArgumentList with UseShellExecute=false (Priority 2)
│ └─ NO → Use escaped arguments with UseShellExecute=false (Priority 2)
│
└─ For ALL solutions:
├─ Add input validation (Priority 3)
└─ Apply least privilege (Priority 4)
Common Vulnerable Patterns
String Concatenation with Process.Start()
// VULNERABLE - Command injection via string concatenation
string filename = Request.QueryString["file"];
Process.Start("cmd.exe", "/c dir " + filename);
// Attack example:
// Input: "file.txt & del /f /q C:\*"
// Result: Deletes all files in C:\
Why this is vulnerable: String concatenation in process arguments allows attackers to inject shell metacharacters (&, |, ;, etc.) that execute arbitrary commands when the shell interprets them.
Using PowerShell with User Input
// VULNERABLE - PowerShell command injection
string script = $"Get-Content {userInput}";
Process.Start("powershell.exe", "-Command " + script);
// Attack example:
// Input: "file.txt; Remove-Item -Recurse C:\"
// Result: Recursively deletes C:\ drive
Why this is vulnerable: PowerShell interprets semicolons and other operators as command separators, allowing attackers to inject additional PowerShell commands that execute with application privileges.
Shell=True Allows Command Injection
// VULNERABLE - UseShellExecute invokes shell
var psi = new ProcessStartInfo()
{
FileName = "ping",
Arguments = "-n 4 " + ipAddress,
UseShellExecute = true // Dangerous!
};
Process.Start(psi);
// Attack example:
// Input: "8.8.8.8 && net user hacker password /add"
// Result: Creates a new admin user
Why this is vulnerable: Setting UseShellExecute=true processes arguments through cmd.exe, enabling shell injection via metacharacters (&&, ||, |, etc.) that chain multiple commands.
Unvalidated Input in Process Arguments
// VULNERABLE - No input validation
string userFile = Request.Form["filepath"];
var psi = new ProcessStartInfo("notepad.exe", userFile);
Process.Start(psi);
// Attack example:
// Input: "C:\data.txt & whoami > C:\inetpub\wwwroot\whoami.txt"
// Result: Executes whoami and writes output to web root
Why this is vulnerable: Without input validation or shell=false protection, user input can contain command injection payloads that execute when passed to external processes.
Secure Patterns
Use .NET Native APIs (PREFERRED - Eliminates Command Injection)
// SECURE - Use .NET APIs instead of OS commands
string[] files = Directory.GetFiles(path);
foreach (string file in files)
{
FileInfo fi = new FileInfo(file);
Console.WriteLine($"{fi.Name} {fi.Length} {fi.LastWriteTime}");
}
// More file operations
string content = File.ReadAllText(filepath); // Instead of "type"
File.Copy(source, dest); // Instead of "copy"
Directory.CreateDirectory(path); // Instead of "mkdir"
File.Delete(filepath); // Instead of "del"
Why this works: Using .NET's built-in APIs completely eliminates command injection vulnerabilities by avoiding OS command execution entirely. These APIs operate directly on system resources without invoking shells, preventing attackers from injecting malicious commands through special characters or command separators.
Use HttpClient for Network Operations
// SECURE - Use HttpClient instead of curl/wget
using (var client = new HttpClient())
{
var response = await client.GetAsync(url);
string content = await response.Content.ReadAsStringAsync();
}
// For downloads
using (var client = new HttpClient())
using (var stream = await client.GetStreamAsync(url))
using (var fileStream = File.Create(localPath))
{
await stream.CopyToAsync(fileStream);
}
Why this works: HttpClient performs network operations through managed .NET code without invoking curl, wget, or other command-line utilities. This eliminates the attack surface entirely - there's no shell to inject commands into, no process arguments to escape, and no possibility of command chaining through metacharacters.
Use System.IO.Compression for Archives
// SECURE - Use built-in compression instead of 7z/zip commands
using (ZipArchive archive = ZipFile.OpenRead(zipPath))
{
foreach (ZipArchiveEntry entry in archive.Entries)
{
string destinationPath = Path.Combine(extractPath, entry.FullName);
entry.ExtractToFile(destinationPath);
}
}
// Creating archives
ZipFile.CreateFromDirectory(sourceDir, zipPath);
Why this works: .NET's compression libraries handle archive operations in managed code without calling 7z.exe, zip, or tar commands. By processing files directly through the framework, you eliminate shell invocation and prevent attackers from injecting commands through malicious filenames or archive paths.
Use String/Regex APIs for Text Processing
// SECURE - Use .NET string operations instead of grep/findstr
string content = File.ReadAllText(filepath);
var matches = Regex.Matches(content, pattern);
// Line-by-line processing
var lines = File.ReadLines(filepath)
.Where(line => line.Contains(searchTerm));
Why this works: .NET's string and regex APIs provide powerful text processing capabilities without invoking grep, findstr, sed, or awk commands. Processing text in-memory through managed code prevents command injection while offering better performance and cross-platform compatibility than shell utilities.
ProcessStartInfo with Argument List (.NET 6+ - If Process Execution Required)
WARNING: Avoid executing OS commands if at all possible. This pattern is ONLY for cases where no native .NET API exists for the required functionality. Always exhaust all .NET alternatives first.
// USE WITH CAUTION - When process execution is unavoidable, use ArgumentList
string ipAddress = Request.QueryString["ip"];
// Validate input first
if (!System.Net.IPAddress.TryParse(ipAddress, out _))
{
throw new ArgumentException("Invalid IP address");
}
// Use ArgumentList - NO SHELL
var psi = new ProcessStartInfo
{
FileName = "ping",
UseShellExecute = false,
RedirectStandardOutput = true,
CreateNoWindow = true
};
psi.ArgumentList.Add("-n");
psi.ArgumentList.Add("4");
psi.ArgumentList.Add(ipAddress); // Arguments are properly escaped
using (var process = Process.Start(psi))
{
string output = process.StandardOutput.ReadToEnd();
process.WaitForExit();
}
Why this works: ArgumentList treats each element as a separate argument, automatically handling escaping and preventing shell interpretation. With UseShellExecute = false, arguments bypass cmd.exe entirely - even malicious characters like &, |, ; are treated as literal data rather than command separators. Input validation provides defense-in-depth.
ProcessStartInfo with Escaped Arguments (.NET Framework)
WARNING: This pattern is inherently risky. Prefer native .NET APIs. Only use when no alternative exists AND you cannot upgrade to .NET 6+ for ArgumentList.
// RISKY - For legacy third-party tools when ArgumentList not available
string inputFile = Request.QueryString["file"];
string outputFile = Request.QueryString["output"];
// Validate filenames (allowlist approach)
if (!Regex.IsMatch(inputFile, @"^[a-zA-Z0-9._-]+$") ||
!Regex.IsMatch(outputFile, @"^[a-zA-Z0-9._-]+$"))
{
throw new ArgumentException("Invalid filename");
}
// Example: calling a legacy report generator or third-party tool
var psi = new ProcessStartInfo
{
FileName = @"C:\Tools\LegacyReportGenerator.exe",
Arguments = $"\"{inputFile}\" \"{outputFile}\"", // Quoted to prevent injection
UseShellExecute = false,
RedirectStandardOutput = true,
WorkingDirectory = @"C:\Data"
};
using (var process = Process.Start(psi))
{
string output = process.StandardOutput.ReadToEnd();
process.WaitForExit();
}
Why this works: Quoting arguments and setting UseShellExecute = false prevents shell interpretation of special characters. Hardcoding the executable path prevents attackers from executing arbitrary programs. Input validation with allowlisting (alphanumeric + safe characters only) blocks command injection attempts. However, ArgumentList (.NET 6+) is preferred as it handles escaping automatically and more reliably. Note: For file operations, always use .NET APIs like File.ReadAllText() instead.
PowerShell Execution (Use with Extreme Caution)
⛔ STRONGLY DISCOURAGED: PowerShell execution introduces significant security risk. In almost all cases, there is a better .NET alternative. Only proceed if you have exhausted all other options and have explicit security approval.
If you absolutely must use PowerShell:
- Use constrained language mode
- Allowlist commands
- Never use -Command with user input
- Prefer PowerShell SDK (System.Management.Automation) over process execution
// ⛔ AVOID IF POSSIBLE - If PowerShell is absolutely necessary
var psi = new ProcessStartInfo
{
FileName = "powershell.exe",
UseShellExecute = false,
RedirectStandardOutput = true
};
// Use -File instead of -Command with hardcoded script
psi.ArgumentList.Add("-ExecutionPolicy");
psi.ArgumentList.Add("Restricted");
psi.ArgumentList.Add("-File");
psi.ArgumentList.Add("C:\\Scripts\\approved-script.ps1");
psi.ArgumentList.Add(validatedParameter); // Only pass validated params
using (var process = Process.Start(psi))
{
// ...
}
Why this works: Using -File instead of -Command prevents PowerShell from interpreting user input as code. The hardcoded script path and restricted execution policy limit what can be executed. ArgumentList ensures parameters are passed as data, not executable commands. However, avoid PowerShell entirely if possible - .NET APIs are safer.
Argument Escaping Utility (When ArgumentList Not Available)
WARNING: Manual escaping is error-prone and has been the source of many vulnerabilities. Use native APIs instead. This is a LAST RESORT only.
For .NET Framework when ArgumentList property is not available.
// ERROR-PRONE - Escape utility for cmd.exe arguments (avoid if possible)
public static string EscapeCommandLineArgument(string argument)
{
// Escape for cmd.exe
if (string.IsNullOrEmpty(argument))
return "\"\"";
// Check if escaping needed
if (!Regex.IsMatch(argument, @"[^\w@%+=:,./\\-]"))
return argument;
// Escape special characters
argument = Regex.Replace(argument, @"(\\*)" + "\"", @"$1$1\""");
argument = Regex.Replace(argument, @"(\\+)$", @"$1$1");
return "\"" + argument + "\"";
}
// Usage
string safeArg = EscapeCommandLineArgument(userInput);
var psi = new ProcessStartInfo("program.exe", safeArg);
psi.UseShellExecute = false;
Process.Start(psi);
Why this works: This utility properly escapes backslashes and quotes according to cmd.exe argument parsing rules, wrapping the result in double quotes. Combined with UseShellExecute = false, it prevents the shell from interpreting special characters. However, this is error-prone - prefer ArgumentList (.NET 6+) or avoiding process execution entirely.
Input Validation (Defense in Depth)
Allowlist Validation
public class InputValidator
{
public static bool IsValidFilename(string filename)
{
// Only allow alphanumeric, underscore, dash, dot
return Regex.IsMatch(filename, @"^[a-zA-Z0-9._-]+$");
}
public static bool IsValidIPAddress(string ip)
{
return System.Net.IPAddress.TryParse(ip, out _);
}
}
ASP.NET Core Model Validation
public class PingRequest
{
[Required]
[RegularExpression(@"^([0-9]{1,3}\.){3}[0-9]{1,3}$",
ErrorMessage = "Invalid IP address")]
public string IpAddress { get; set; }
}
[HttpPost("ping")]
public IActionResult Ping([FromBody] PingRequest request)
{
if (!ModelState.IsValid)
return BadRequest(ModelState);
// Input is validated - safe to use with ArgumentList
var psi = new ProcessStartInfo("ping");
psi.ArgumentList.Add("-n");
psi.ArgumentList.Add("4");
psi.ArgumentList.Add(request.IpAddress);
// ... execute
}
Security Best Practices
Principle of Least Privilege
var psi = new ProcessStartInfo
{
FileName = "ping",
UserName = "restricted_user", // Run as low-privilege user
Domain = "DOMAIN",
UseShellExecute = false,
RedirectStandardOutput = true
};
// Set password securely
using (var password = GetPasswordFromSecureStore())
{
psi.Password = password;
using (var process = Process.Start(psi))
{
// ...
}
}
Dependencies and Installation
.NET Version Requirements
.NET 6 or Later (Recommended):
- Full support for
ArgumentListproperty - Modern security features
- Best command injection protection
.NET Framework 4.5+:
- Use argument escaping instead of
ArgumentList - Requires manual escaping logic
- Consider upgrading to .NET 6+
NuGet Packages
<!-- For .NET Framework projects needing compression -->
<PackageReference Include="System.IO.Compression" />
<PackageReference Include="System.IO.Compression.ZipFile" />
<!-- For HTTP operations in older frameworks -->
<PackageReference Include="System.Net.Http" />
Configuration (Optional - Security Hardening)
<!-- web.config or app.config -->
<configuration>
<system.diagnostics>
<trace autoflush="true">
<listeners>
<add name="processMonitor"
type="System.Diagnostics.TextWriterTraceListener"
initializeData="process-audit.log" />
</listeners>
</trace>
</system.diagnostics>
</configuration>