Review processes started with a composed arguments string
Matches methods that start a process with Process.Start(fileName, arguments), new ProcessStartInfo(fileName, arguments) or by assigning ProcessStartInfo.Arguments, while also building strings (String.Concat(), Format(), Join(), interpolation, StringBuilder) and never using the ArgumentList collection. A single arguments string is re-parsed by the target program (or by the shell when UseShellExecute is true): quotes, spaces, &, | or ; in an interpolated value change the command executed.
This is CWE-78 OS Command Injection (review: the rule cannot tell whether the composed fragments are user-controlled).
Flagged Code
This is CWE-78 OS Command Injection (review: the rule cannot tell whether the composed fragments are user-controlled).
Flagged Code
public void Convert(string inputFile) {
// flagged: re-parsed by ffmpeg, a quote in inputFile injects extra arguments
var arguments = $"-i \"{inputFile}\" -o output.mp4";
Process.Start("ffmpeg", arguments);
}
Fixed Codepublic void Convert(string inputFile) {
var startInfo = new ProcessStartInfo("ffmpeg") { UseShellExecute = false };
startInfo.ArgumentList.Add("-i");
startInfo.ArgumentList.Add(inputFile); // quoted per argument, nothing is re-parsed
startInfo.ArgumentList.Add("-o");
startInfo.ArgumentList.Add("output.mp4");
Process.Start(startInfo);
}How to fix
Pass each argument through ProcessStartInfo.ArgumentList.Add(...) (escaping is then done per argument), keep UseShellExecute = false, and validate or whitelist any value that comes from a request, a file name or configuration. Suppress the issue when every composed fragment is a constant or an internally generated path.
CQLinq source
This rule is written in CQLinq, C# LINQ over the NDepend code model. It ships with every new NDepend project: you can read it, adapt it, or use it as a starting point for your own custom rules.
// <Name>Review processes started with a composed arguments string</Name>
// <Id>ND3124:ReviewProcessesStartedWithComposedArgumentsString</Id>
warnif count > 0
// Third-party members are part of the code model only when the application uses them.
// Members taking the arguments as a single string that the target program (or the shell) re-parses:
let processStringMembers = ThirdParty.Types.WithFullNameIn("System.Diagnostics.Process", "System.Diagnostics.ProcessStartInfo")
.ChildMethods().Where(m =>
(m.SimpleName == "Start" && m.IsStatic && m.Name.StartsWith("Start(String,String")) || // Process.Start(fileName, arguments)
(m.IsConstructor && m.Name.StartsWith(".ctor(String,String")) || // new ProcessStartInfo(fileName, arguments)
m.SimpleName == "set_Arguments") // startInfo.Arguments = ...
.ToHashSetEx()
where processStringMembers.Any()
// ArgumentList quotes each argument: the safe API.
let argumentListGetter = ThirdParty.Types.WithFullName("System.Diagnostics.ProcessStartInfo")
.ChildMethods().Where(m => m.SimpleName == "get_ArgumentList").ToHashSetEx()
// String composition in the same method: concatenation, formatting, interpolation, builders.
let stringBuildingMembers = ThirdParty.Methods.Where(m =>
(m.ParentType.FullName == "System.String" && m.SimpleNameEqualsAny("Concat", "Format", "Join", "Replace", "Insert")) ||
m.ParentType.FullNameEqualsAny("System.Text.StringBuilder", "System.Runtime.CompilerServices.DefaultInterpolatedStringHandler"))
.ToHashSetEx()
from m in Application.Methods.UsingAny(processStringMembers)
where !m.MethodsCalled.Intersect(argumentListGetter).Any()
let processMembersUsed = m.MethodsCalled.Intersect(processStringMembers).ToArray()
let composesStrings = m.MethodsCalled.Intersect(stringBuildingMembers).Any()
where composesStrings
select new {
m,
processMembersUsed,
Debt = 15.ToMinutes().ToDebt(),
Severity = Severity.Medium
}
//<Expl>
// The *{0}* starts a process through *{1}* with an arguments string
// composed in the method. If any fragment derives from user input, a
// crafted value can inject extra arguments or commands.
//</Expl>
//<Desc>
// Matches methods that start a process with *Process.Start(fileName, arguments)*,
// *new ProcessStartInfo(fileName, arguments)* or by assigning
// *ProcessStartInfo.Arguments*, while also building strings
// (*String.Concat()*, *Format()*, *Join()*, interpolation, *StringBuilder*)
// and never using the *ArgumentList* collection. A single arguments
// string is re-parsed by the target program (or by the shell when
// *UseShellExecute* is true): quotes, spaces, *&*, *|* or *;* in an
// interpolated value change the command executed.
//
// This is CWE-78 *OS Command Injection* (review: the rule cannot tell
// whether the composed fragments are user-controlled).
//
// **Flagged Code**
// ```csharp
// public void Convert(string inputFile) {
// // flagged: re-parsed by ffmpeg, a quote in inputFile injects extra arguments
// var arguments = $"-i \"{inputFile}\" -o output.mp4";
// Process.Start("ffmpeg", arguments);
// }
// ```
//
// **Fixed Code**
// ```csharp
// public void Convert(string inputFile) {
// var startInfo = new ProcessStartInfo("ffmpeg") { UseShellExecute = false };
// startInfo.ArgumentList.Add("-i");
// startInfo.ArgumentList.Add(inputFile); // quoted per argument, nothing is re-parsed
// startInfo.ArgumentList.Add("-o");
// startInfo.ArgumentList.Add("output.mp4");
// Process.Start(startInfo);
// }
// ```
//</Desc>
//<HowToFix>
// Pass each argument through *ProcessStartInfo.ArgumentList.Add(...)*
// (escaping is then done per argument), keep *UseShellExecute = false*,
// and validate or whitelist any value that comes from a request, a file
// name or configuration. Suppress the issue when every composed fragment
// is a constant or an internally generated path.
//</HowToFix>
Open in the interactive Rules Explorer · All default rules
Security: all 39 items
- Software Composition Analysis (SCA) ND3120
- Transitive Software Composition Analysis (SCA) ND3121
- Don't hard-code secrets in constants ND3114
- Review credentials built from hard-coded values ND3115
- Don't use insecure deserializers ND3110
- Don't use weak hash algorithms (MD5, SHA1) ND3109
- Don't use DES/3DES/RC2 weak cipher algorithms ND3102
- Don't use weak password-based key derivation ND3123
- Don't use the ECB encryption mode ND3132
- Don't disable certificate validation ND3103
- Don't hard-code TLS/SSL protocol versions ND3122
- Review commands vulnerable to SQL injection ND3107
- Review data adapters vulnerable to SQL injection ND3108
- Review raw SQL built from composed strings ND3128
- Review LDAP filters composed from strings ND3137
- Don't enable external XML entity resolution (XXE) ND3111
- Review processes started with a composed arguments string ND3124
- Review web endpoints building file paths from their parameters ND3129
- Review custom archive extraction (Zip Slip) ND3135
- Review assemblies and types loaded from runtime-provided names ND3125
- Review web endpoints without [Authorize] nor [AllowAnonymous] ND3118
- Review state-changing endpoints opened to anonymous callers ND3130
- Review state-changing MVC actions without anti-forgery validation ND3119
- Review HTML-encoding bypasses ND3133
- Review explicitly disabled request validation ND3134
- Review redirects to a URL that is not validated as local ND3126
- Review request-influenced outbound HTTP calls (SSRF) ND3136
- Review insecure ASP.NET Core CORS policies and unguarded developer exception page ND3116
- Review cookies created without HttpOnly and Secure flags ND3117
- Review regular expressions without a match timeout (ReDoS) ND3112
- Review secrets compared with non-constant-time equality ND3113
- Review secrets passed to logging methods ND3127
- Review security operations that log nothing ND3131
- Don't use System.Random for security purposes ND3101
- Don't use CoSetProxyBlanket and CoInitializeSecurity ND3100
- Review publicly visible event handlers ND3104
- Pointers should not be publicly visible ND3105
- Seal methods that satisfy non-public interfaces ND3106
- Restrict sensitive APIs to reviewed components ND3138