CWE-77: Command Injection - C# / .NET
Overview
Command injection in .NET occurs when applications construct system commands using untrusted input. The Process class and related APIs can invoke shells, allowing attackers to execute arbitrary commands.
Primary Defence: Avoid shell execution entirely by using ProcessStartInfo with UseShellExecute = false and passing arguments as an array instead of concatenating strings, validate all user input against strict allowlists (e.g., alphanumeric patterns), use parameterized approaches like built-in .NET libraries for specific tasks instead of shelling out, and implement least privilege by running processes with minimal permissions to prevent command injection attacks.
Common Vulnerable Patterns
Process.Start() with String Concatenation
// VULNERABLE - User input in command
using System.Diagnostics;
string ipAddress = Request.QueryString["ip"];
Process.Start("cmd.exe", "/c ping " + ipAddress);
// Attack: ip=8.8.8.8 & whoami
// Executes: ping 8.8.8.8 & whoami
ProcessStartInfo with UseShellExecute
// VULNERABLE - Shell execution enabled
string fileName = Request.Form["file"];
var startInfo = new ProcessStartInfo
{
FileName = "cmd.exe",
Arguments = "/c type " + fileName,
UseShellExecute = true // DANGEROUS!
};
Process.Start(startInfo);
// Attack: file=data.txt & del /F /Q *.*
PowerShell with User Input
// VULNERABLE - PowerShell command injection
string domain = Request.QueryString["domain"];
var psi = new ProcessStartInfo
{
FileName = "powershell.exe",
Arguments = $"-Command Resolve-DnsName {domain}"
};
Process.Start(psi);
// Attack: domain=example.com; Remove-Item -Recurse C:\Temp\*
Command String Concatenation
// VULNERABLE - String interpolation in commands
string host = Request.Form["host"];
Process.Start(new ProcessStartInfo
{
FileName = "ping",
Arguments = $"{host} -n 4"
});
// Attack: host=8.8.8.8 && netsh firewall set opmode mode=disable
Secure Patterns
Use .NET Native APIs (Primary Defense)
// SECURE - Use .NET libraries instead of system commands
using System;
using System.Net;
using System.Net.NetworkInformation;
using System.IO;
using System.Text.RegularExpressions;
// Instead of: Process.Start("ping", host)
public class NetworkHelper
{
public static bool IsHostReachable(string hostname)
{
// Validate hostname
if (!Regex.IsMatch(hostname, @"^[a-zA-Z0-9.-]+$"))
{
throw new ArgumentException("Invalid hostname");
}
try
{
using (var ping = new Ping())
{
var reply = ping.Send(hostname, 5000);
return reply.Status == IPStatus.Success;
}
}
catch (PingException)
{
return false;
}
}
}
// Instead of: Process.Start("cmd.exe", "/c del " + file)
public class FileHelper
{
public static void DeleteFile(string fileName)
{
// Validate filename
if (!Regex.IsMatch(fileName, @"^[a-zA-Z0-9_.-]+$"))
{
throw new ArgumentException("Invalid filename");
}
string safePath = Path.Combine(@"C:\SafeDirectory", fileName);
// Ensure file is within safe directory
string fullPath = Path.GetFullPath(safePath);
if (!fullPath.StartsWith(@"C:\SafeDirectory\"))
{
throw new ArgumentException("Path traversal attempt");
}
File.Delete(fullPath);
}
}
// Instead of: Process.Start("curl", url)
public class HttpHelper
{
public static async Task<string> FetchUrlAsync(string url)
{
using (var client = new HttpClient())
{
client.Timeout = TimeSpan.FromSeconds(10);
return await client.GetStringAsync(url);
}
}
}
// Instead of: Process.Start("powershell", "-Command Compress-Archive")
public class ArchiveHelper
{
public static void CreateZipArchive(string[] files, string archivePath)
{
using (var archive = System.IO.Compression.ZipFile.Open(
archivePath,
System.IO.Compression.ZipArchiveMode.Create))
{
foreach (string file in files)
{
// Validate each filename
if (!Regex.IsMatch(file, @"^[a-zA-Z0-9_.\\/-]+$"))
{
continue;
}
archive.CreateEntryFromFile(file, Path.GetFileName(file));
}
}
}
}
Why this works:
- Native .NET APIs eliminate shell execution:
Ping.Send()uses ICMP sockets,HttpClientuses managed HTTP,ZipFileuses native compression - No attack surface for shell metacharacters: Bypassing shell removes risk of
;,&,|, backticks,$() - Regex validation provides defense-in-depth: Hostname/filename patterns block path traversal and command separators
- Path canonicalization prevents directory traversal:
Path.Combine+Path.GetFullPath+.StartsWith()ensures files stay in safe directory - Cross-platform and type-safe: Managed libraries work consistently across OS, avoiding shell-specific escaping quirks
- Better error handling: Exceptions provide clear error information vs parsing command output
Process with Argument Escaping
// SECURE - Disable shell execution and validate inputs
using System.Diagnostics;
using System.Text.RegularExpressions;
public class SecureProcessExecutor
{
public static string ExecutePing(string ipAddress)
{
// Validate IP address format
string ipPattern = @"^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}" +
@"(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$";
if (!Regex.IsMatch(ipAddress, ipPattern))
{
throw new ArgumentException("Invalid IP address");
}
var startInfo = new ProcessStartInfo
{
FileName = "ping",
Arguments = $"{ipAddress} -n 4",
UseShellExecute = false, // CRITICAL: No shell
RedirectStandardOutput = true,
CreateNoWindow = true
};
using (var process = Process.Start(startInfo))
{
return process.StandardOutput.ReadToEnd();
}
}
public static string ExecuteNslookup(string domain)
{
// Validate domain name
if (!Regex.IsMatch(domain, @"^[a-zA-Z0-9.-]+$"))
{
throw new ArgumentException("Invalid domain name");
}
if (domain.Length > 253)
{
throw new ArgumentException("Domain name too long");
}
var startInfo = new ProcessStartInfo
{
FileName = "nslookup",
Arguments = domain,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
using (var process = Process.Start(startInfo))
{
process.WaitForExit(5000);
if (!process.HasExited)
{
process.Kill();
throw new TimeoutException("Command timed out");
}
return process.StandardOutput.ReadToEnd();
}
}
}
Why this works:
UseShellExecute = falsebypasses shell: Directly invokes executables viaCreateProcess(), preventing metacharacter interpretation- Arguments treated as literals: With shell disabled,
;,&,|, backticks,$()are strings, not commands - Strict regex validation blocks injection: IP pattern ensures only valid IPs; domain pattern prevents separators/traversal
- Redirects prevent UI leaks:
RedirectStandardOutput+CreateNoWindowavoid interactive prompts or visual pop-ups - Timeout prevents DoS:
WaitForExit(5000)+Kill()stops slow-resolving domains or infinite loops - Length limits prevent buffer overflow: Domain ≤ 253 chars per RFC 1035 protects older DNS utilities
Input Validation Class
// SECURE - Comprehensive input validation
using System.Text.RegularExpressions;
public static class InputValidator
{
private static readonly Regex HostnamePattern =
new Regex(@"^[a-zA-Z0-9.-]+$", RegexOptions.Compiled);
private static readonly Regex IpPattern =
new Regex(@"^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}" +
@"(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$",
RegexOptions.Compiled);
private static readonly Regex FilenamePattern =
new Regex(@"^[a-zA-Z0-9_.-]+$", RegexOptions.Compiled);
private static readonly Regex NumberPattern =
new Regex(@"^[0-9]+$", RegexOptions.Compiled);
public static string ValidateHostname(string input)
{
if (string.IsNullOrWhiteSpace(input))
{
throw new ArgumentException("Hostname cannot be empty");
}
if (input.Length > 253)
{
throw new ArgumentException("Hostname too long");
}
if (!HostnamePattern.IsMatch(input))
{
throw new ArgumentException("Invalid hostname format");
}
return input;
}
public static string ValidateIPAddress(string input)
{
if (!IpPattern.IsMatch(input))
{
throw new ArgumentException("Invalid IP address format");
}
return input;
}
public static string ValidateFilename(string input)
{
if (string.IsNullOrWhiteSpace(input))
{
throw new ArgumentException("Filename cannot be empty");
}
if (input.Contains("..") || input.Contains("/") || input.Contains("\\"))
{
throw new ArgumentException("Path traversal attempt detected");
}
if (!FilenamePattern.IsMatch(input))
{
throw new ArgumentException("Invalid filename format");
}
return input;
}
public static int ValidateNumber(string input)
{
if (!NumberPattern.IsMatch(input))
{
throw new ArgumentException("Invalid number format");
}
return int.Parse(input);
}
}
Why this works:
- Centralized validation: Compiled regex (
RegexOptions.Compiled) improves performance for repeated validations; ensures consistency across all command execution paths - Allowlist validation: Only alphanumerics, hyphens, dots for hostnames blocks shell metacharacters (
&,|,;,>,<, backticks,$(), newlines) - Security checks: Path traversal detection (
Contains(".."),/,\\), length limits (≤253 chars) prevent attacks and DoS; null/whitespace checks prevent empty strings - Number validation:
^[0-9]+$+int.Parseprevents arithmetic injection ($(expr 1 + 1)), ensures predictable numeric arguments (ports, counts, timeouts) - Testable design: Fail-fast exceptions enable unit testing validation logic separately from command execution
Safe Process Execution Wrapper
// SECURE - Complete safe execution pattern
using System.Diagnostics;
using System.Collections.Generic;
public class SafeCommandExecutor
{
// Allowlist of permitted commands
private static readonly Dictionary<string, string> AllowedCommands =
new Dictionary<string, string>
{
{ "ping", @"C:\Windows\System32\ping.exe" },
{ "nslookup", @"C:\Windows\System32\nslookup.exe" },
{ "tracert", @"C:\Windows\System32\tracert.exe" }
};
public static string ExecuteCommand(string command, params string[] arguments)
{
// Verify command is allowed
if (!AllowedCommands.ContainsKey(command))
{
throw new ArgumentException($"Command not allowed: {command}");
}
var startInfo = new ProcessStartInfo
{
FileName = AllowedCommands[command],
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
// Add validated arguments
foreach (string arg in arguments)
{
startInfo.ArgumentList.Add(arg);
}
using (var process = new Process { StartInfo = startInfo })
{
process.Start();
// Set timeout
if (!process.WaitForExit(30000))
{
process.Kill();
throw new TimeoutException("Command execution timeout");
}
return process.StandardOutput.ReadToEnd();
}
}
}
Why this works:
- Command allowlist:
Dictionarywith absolute paths (C:\Windows\System32\ping.exe) prevents PATH manipulation;ContainsKeyensures only permitted commands run - ArgumentList isolation:
ArgumentList.Add()(.NET Core 2.1+) passes arguments as string array - each treated as literal regardless of spaces/special characters - Direct invocation:
UseShellExecute = falseensures direct process invocation without shell interpretation - DoS prevention: 30-second timeout +
Kill()prevents infinite hangs (e.g.,ping -t);RedirectStandardErrorcaptures errors without exposing to users - Reusable wrapper: Defense-in-depth combining allowlisting, argument isolation, timeouts, and error handling for audit and policy enforcement
// VULNERABLE Controller Action
using Microsoft.AspNetCore.Mvc;
using System.Diagnostics;
public class DiagnosticsController : Controller
{
[HttpGet]
public IActionResult Ping(string host)
{
// VULNERABLE
var result = Process.Start("cmd.exe", $"/c ping {host}");
return Ok();
}
}
// SECURE Controller Action
using Microsoft.AspNetCore.Mvc;
using System.Net.NetworkInformation;
public class DiagnosticsController : Controller
{
[HttpGet]
public IActionResult Ping(string host)
{
try
{
// Validate input
host = InputValidator.ValidateHostname(host);
// Use .NET API instead of system command
using (var ping = new Ping())
{
var reply = ping.Send(host, 5000);
return Ok(new
{
Success = reply.Status == IPStatus.Success,
RoundtripTime = reply.RoundtripTime,
Address = reply.Address?.ToString()
});
}
}
catch (ArgumentException ex)
{
return BadRequest(ex.Message);
}
catch (PingException ex)
{
return StatusCode(500, "Ping failed: " + ex.Message);
}
}
}
Common .NET Command Injection Scenarios
Network Diagnostics
// VULNERABLE
string host = Request.Query["host"];
Process.Start("tracert", host);
// SECURE - Use Traceroute alternative or validate strictly
public async Task<string> TraceRoute(string hostname)
{
hostname = InputValidator.ValidateHostname(hostname);
var startInfo = new ProcessStartInfo
{
FileName = @"C:\Windows\System32\tracert.exe",
Arguments = $"-h 30 {hostname}",
UseShellExecute = false,
RedirectStandardOutput = true,
CreateNoWindow = true
};
using (var process = Process.Start(startInfo))
{
if (!process.WaitForExit(60000))
{
process.Kill();
throw new TimeoutException();
}
return await process.StandardOutput.ReadToEndAsync();
}
}
File Conversion
// VULNERABLE
string file = Request.Form["file"];
Process.Start("magick", $"convert {file} output.pdf");
// SECURE - Use .NET image processing library
using ImageMagick;
public void ConvertImage(string inputFile)
{
string safeFile = InputValidator.ValidateFilename(inputFile);
string fullPath = Path.Combine(@"C:\Uploads", safeFile);
using (var image = new MagickImage(fullPath))
{
image.Format = MagickFormat.Pdf;
image.Write("output.pdf");
}
}
Verification
After implementing the recommended secure patterns, verify the fix through multiple approaches:
- Manual testing: Submit malicious payloads relevant to this vulnerability and confirm they're handled safely without executing unintended operations
- Code review: Confirm all instances use the secure pattern (parameterized queries, safe APIs, proper encoding) with no string concatenation or unsafe operations
- Static analysis: Use security scanners to verify no new vulnerabilities exist and the original finding is resolved
- Regression testing: Ensure legitimate user inputs and application workflows continue to function correctly
- Edge case validation: Test with special characters, boundary conditions, and unusual inputs to verify proper handling
- Framework verification: If using a framework or library, confirm the recommended APIs are used correctly according to documentation
- Authentication/session testing: Verify security controls remain effective and cannot be bypassed (if applicable to the vulnerability type)
- Rescan: Run the security scanner again to confirm the finding is resolved and no new issues were introduced