Skip to content

Commit c8b2fda

Browse files
authored
Merge pull request #278 from microsoft/powershell-cmd-injection-updates
Powershell command injection updates
2 parents bf7ebcc + 44ed048 commit c8b2fda

File tree

7 files changed

+308
-95
lines changed

7 files changed

+308
-95
lines changed

powershell/ql/lib/semmle/code/powershell/security/CommandInjectionCustomizations.qll

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,19 @@ module CommandInjection {
3434
class FlowSourceAsSource extends Source {
3535
FlowSourceAsSource() {
3636
this instanceof SourceNode and
37-
not this instanceof EnvironmentVariableSource
37+
not this instanceof EnvironmentVariableSource and
38+
not this instanceof InvokeWebRequest
3839
}
3940

4041
override string getSourceType() { result = "user-provided value" }
4142
}
4243

44+
class InvokeWebRequest extends DataFlow::CallNode {
45+
InvokeWebRequest(){
46+
this.matchesName("Invoke-WebRequest")
47+
}
48+
}
49+
4350
/**
4451
* A command argument to a function that initiates an operating system command.
4552
*/
@@ -60,6 +67,16 @@ module CommandInjection {
6067
override string getSinkType() { result = "call to Invoke-Expression" }
6168
}
6269

70+
class StartProcessSink extends Sink {
71+
StartProcessSink(){
72+
exists(DataFlow::CallNode call |
73+
call.matchesName("Start-Process") and
74+
call.getAnArgument() = this
75+
)
76+
}
77+
override string getSinkType(){ result = "call to Start-Process"}
78+
}
79+
6380
class AddTypeSink extends Sink {
6481
AddTypeSink() {
6582
exists(DataFlow::CallNode call |
@@ -218,6 +235,17 @@ module CommandInjection {
218235
}
219236
}
220237

238+
class ValidateAttributeSanitizer extends Sanitizer {
239+
ValidateAttributeSanitizer() {
240+
exists(Function f, Attribute a, Parameter p |
241+
p = f.getAParameter() and
242+
p.getAnAttribute() = a and
243+
a.getName() = ["ValidateScript", "ValidateSet", "ValidatePattern"] and
244+
this.asParameter() = p
245+
)
246+
}
247+
}
248+
221249
class SingleQuoteSanitizer extends Sanitizer {
222250
SingleQuoteSanitizer() {
223251
exists(ExpandableStringExpr e, VarReadAccess v |
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<!DOCTYPE qhelp PUBLIC
2+
"-//Semmle//qhelp//EN"
3+
"qhelp.dtd">
4+
<qhelp>
5+
<overview>
6+
<p>Code that passes user input directly to
7+
<code>Invoke-Expression</code>, <code>&amp;</code>, or some other library
8+
routine that executes a command, allows the user to execute malicious
9+
code.</p>
10+
11+
<p>The following are considered dangerous sinks: </p>
12+
<ul>
13+
<li>Invoke-Expression</li>
14+
<li>InvokeScript</li>
15+
<li>CreateNestedPipeline</li>
16+
<li>AddScript</li>
17+
<li>powershell</li>
18+
<li>cmd</li>
19+
<li>Foreach-Object</li>
20+
<li>Invoke</li>
21+
<li>CreateScriptBlock</li>
22+
<li>NewScriptBlock</li>
23+
<li>ExpandString</li>
24+
</ul>
25+
26+
</overview>
27+
<recommendation>
28+
29+
<p>If possible, use hard-coded string literals to specify the command to run
30+
or library to load. Instead of passing the user input directly to the
31+
process or library function, examine the user input and then choose
32+
among hard-coded string literals.</p>
33+
34+
<p>If the applicable libraries or commands cannot be determined at
35+
compile time, then add code to verify that the user input string is
36+
safe before using it.</p>
37+
38+
</recommendation>
39+
<example>
40+
41+
<p>The following example shows code that takes a shell script that can be changed
42+
maliciously by a user, and passes it straight to <code>Invoke-Expression</code>
43+
without examining it first.</p>
44+
45+
<sample src="examples/command_injection.ps1" />
46+
47+
</example>
48+
<references>
49+
50+
<li>
51+
OWASP:
52+
<a href="https://www.owasp.org/index.php/Command_Injection">Command Injection</a>.
53+
</li>
54+
<li>
55+
Injection Hunter:
56+
<a href="https://devblogs.microsoft.com/powershell/powershell-injection-hunter-security-auditing-for-powershell-scripts/">PowerShell Injection Hunter: Security Auditing for PowerShell Scripts</a>.
57+
</li>
58+
<!-- LocalWords: CWE untrusted unsanitized Runtime
59+
-->
60+
61+
62+
</references>
63+
</qhelp>
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/**
2+
* @name Uncontrolled command line from param to CmdletBinding
3+
* @description Using externally controlled strings in a command line may allow a malicious
4+
* user to change the meaning of the command.
5+
* @kind path-problem
6+
* @problem.severity error
7+
* @security-severity 9.8
8+
* @precision high
9+
* @id powershell/microsoft/public/command-injection-critical
10+
* @tags correctness
11+
* security
12+
* external/cwe/cwe-078
13+
* external/cwe/cwe-088
14+
*/
15+
16+
import powershell
17+
import semmle.code.powershell.security.CommandInjectionCustomizations::CommandInjection
18+
import semmle.code.powershell.dataflow.TaintTracking
19+
import semmle.code.powershell.dataflow.DataFlow
20+
21+
abstract class CriticalSource extends DataFlow::Node {
22+
/** Gets a string that describes the type of this flow source. */
23+
abstract string getSourceType();
24+
}
25+
26+
class CmdletBindingParam extends CriticalSource {
27+
CmdletBindingParam(){
28+
exists(Attribute a, Function f |
29+
a.getName() = "CmdletBinding" and
30+
f = a.getEnclosingFunction() and
31+
this.asParameter() = f.getAParameter()
32+
)
33+
}
34+
override string getSourceType(){
35+
result = "param to CmdletBinding function"
36+
}
37+
}
38+
39+
40+
private module Config implements DataFlow::ConfigSig {
41+
predicate isSource(DataFlow::Node source) { source instanceof CriticalSource }
42+
43+
predicate isSink(DataFlow::Node sink) { sink instanceof Sink }
44+
45+
predicate isBarrier(DataFlow::Node node) { node instanceof Sanitizer }
46+
}
47+
48+
/**
49+
* Taint-tracking for reasoning about command-injection vulnerabilities.
50+
*/
51+
module CommandInjectionCriticalFlow = TaintTracking::Global<Config>;
52+
import CommandInjectionCriticalFlow::PathGraph
53+
54+
from CommandInjectionCriticalFlow::PathNode source, CommandInjectionCriticalFlow::PathNode sink, CriticalSource sourceNode
55+
where
56+
CommandInjectionCriticalFlow::flowPath(source, sink) and
57+
sourceNode = source.getNode()
58+
select sink.getNode(), source, sink, "This command depends on a $@.", sourceNode,
59+
sourceNode.getSourceType()

0 commit comments

Comments
 (0)