From 656b7343917afbb5f50b07a14ec3725eb801ad76 Mon Sep 17 00:00:00 2001
From: Chanel Young
Date: Thu, 3 Apr 2025 11:23:49 -0700
Subject: [PATCH 1/6] initial query
---
.../InjectionHunter/UserInput.qll | 0
.../UserInputToDangerousMethod.qhelp | 40 ++++
.../UserInputToDangerousMethod.ql | 172 ++++++++++++++++++
3 files changed, 212 insertions(+)
create mode 100644 powershell/ql/src/experimental/InjectionHunter/UserInput.qll
create mode 100644 powershell/ql/src/experimental/InjectionHunter/UserInputToDangerousMethod.qhelp
create mode 100644 powershell/ql/src/experimental/InjectionHunter/UserInputToDangerousMethod.ql
diff --git a/powershell/ql/src/experimental/InjectionHunter/UserInput.qll b/powershell/ql/src/experimental/InjectionHunter/UserInput.qll
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/powershell/ql/src/experimental/InjectionHunter/UserInputToDangerousMethod.qhelp b/powershell/ql/src/experimental/InjectionHunter/UserInputToDangerousMethod.qhelp
new file mode 100644
index 00000000000..f9ffbe53403
--- /dev/null
+++ b/powershell/ql/src/experimental/InjectionHunter/UserInputToDangerousMethod.qhelp
@@ -0,0 +1,40 @@
+
+
+
+Code that passes user input directly to
+Invoke-Expression, &, or some other library
+routine that executes a command, allows the user to execute malicious
+code.
+
+
+
+
+Possible script injection risk via the Invoke-Expression cmdlet. Untrusted input can cause arbitrary PowerShell expressions to be run.
+Variables may be used directly for dynamic parameter arguments, splatting can be used for dynamic parameter names,
+and the invocation operator can be used for dynamic command names. If content escaping is truly needed, PowerShell has several valid quote characters,
+so [System.Management.Automation.Language.CodeGeneration]::Escape* should be used.
+
+
+
+
+The following example shows code that takes a shell script that can be changed
+maliciously by a user, and passes it straight to Invoke-Expression
+without examining it first.
+
+
+
+
+
+
+
+OWASP:
+Command Injection.
+
+
+
+
+
+
diff --git a/powershell/ql/src/experimental/InjectionHunter/UserInputToDangerousMethod.ql b/powershell/ql/src/experimental/InjectionHunter/UserInputToDangerousMethod.ql
new file mode 100644
index 00000000000..bdbcdbddfe3
--- /dev/null
+++ b/powershell/ql/src/experimental/InjectionHunter/UserInputToDangerousMethod.ql
@@ -0,0 +1,172 @@
+/**
+ * @name User Input to Invoke-Expression
+ * @description Finding cases where the user input is passed an Invoke-Expression command
+ * @kind path-problem
+ * @problem.severity error
+ * @security-severity 9.8
+ * @precision high
+ * @id powershell/microsoft/public/user-input-to-invoke-expression
+ * @tags security
+ * external/cwe/cwe-078
+ * external/cwe/cwe-088
+ */
+
+import powershell
+import semmle.code.powershell.dataflow.TaintTracking
+import semmle.code.powershell.dataflow.DataFlow
+import semmle.code.powershell.ApiGraphs
+
+private module TestConfig implements DataFlow::ConfigSig {
+ predicate isSource(DataFlow::Node source) {
+ exists(CmdCall c |
+ c.getName() = "Read-Host" and
+ source.asExpr().getExpr() = c) }
+
+ predicate isSink(DataFlow::Node sink) { sink instanceof Sink }
+ predicate isBarrier(DataFlow::Node node) {node instanceof Sanitizer}
+}
+
+abstract class Source extends DataFlow::Node {}
+
+class ReadHostSource extends Source {
+ ReadHostSource() {
+ exists(CmdCall c |
+ this.asExpr().getExpr() = c and
+ c.getName() = "Read-Host" )
+ }
+}
+
+class GetContentSource extends Source {
+ GetContentSource() {
+ exists(CmdCall c |
+ this.asExpr().getExpr() = c and
+ c.getName() = "Get-Content" )
+ }
+}
+
+class ValueFromPipelineSource extends Source {
+ ValueFromPipelineSource() {
+ exists(Parameter p |
+ p.getAnAttribute().toString() = "ValueFromPipeline" and
+ this.asExpr().getExpr() = p.getAnAccess()
+ )
+ }
+}
+
+abstract class Sink extends DataFlow::Node {}
+
+class InvokeExpressionCall extends Sink {
+ InvokeExpressionCall() {
+ exists(CmdCall c |
+ this.asExpr().getExpr() = c.getAnArgument() and
+ c.getName() = ["Invoke-Expression", "iex", "Add-Type" ] )
+ }
+}
+
+class InvokeScriptSink extends Sink {
+ InvokeScriptSink() {
+ exists(InvokeMemberExpr ie |
+ this.asExpr().getExpr() = ie.getAnArgument() and
+ ie.getName() = "InvokeScript" and
+ ie.getQualifier().toString() = "InvokeCommand" and
+ ie.getQualifier().getAChild().toString() = "executioncontext"
+ )
+ }
+}
+
+class CreateNestedPipelineSink extends Sink {
+ CreateNestedPipelineSink() {
+ exists(InvokeMemberExpr ie |
+ this.asExpr().getExpr() = ie.getAnArgument() and
+ ie.getName() = "CreateNestedPipeline" and
+ ie.getQualifier().toString() = "InvokeCommand" and
+ ie.getQualifier().getAChild().toString() = "executioncontext")
+ }
+}
+
+class AddScriptInvokeSink extends Sink {
+ AddScriptInvokeSink() {
+ exists(InvokeMemberExpr ie |
+ this.asExpr().getExpr() = ie.getAnArgument() and
+ ie.getName() = "AddScript" and
+ ie.getQualifier().(InvokeMemberExpr).getName() = "Create" and
+ ie.getQualifier().getAChild().toString() = "PowerShell" and
+ ie.getParent().(InvokeMemberExpr).getName() = "Invoke"
+ )
+ }
+}
+
+abstract class Sanitizer extends DataFlow::Node {}
+
+// class TypedParameterSanitizer extends Sanitizer{
+// TypedParameterSanitizer() {
+// exists(Function f, CmdCall c, Parameter p, Argument a |
+// p = f.getAParameter() and
+// a = c.getAnArgument() and
+// p.getName().toLowerCase() = a.getName() and
+// p.getStaticType() != "Object" and
+// c.getName() = f.getName() and
+
+// this.asExpr().getExpr() = a
+// )
+// }
+// }
+
+class SingleQuoteSanitizer extends Sanitizer {
+ SingleQuoteSanitizer() {
+ exists(Expr e, VarReadAccess v |
+ e = this.asExpr().getExpr().getParent() and
+ e.toString().matches("%'$" + v.getVariable().getName() + "'%")
+ )
+ }
+}
+
+module TestFlow = TaintTracking::Global;
+import TestFlow::PathGraph
+
+// from TestFlow::PathNode source, TestFlow::PathNode sink
+// where
+// TestFlow::flowPath(source, sink) and
+// sink.getNode().asExpr().getExpr().getLocation().getFile().getBaseName() = "sanitizers.ps1"
+// select sink.getNode(), source, sink, "Flow from user input to Invoke-Expression"
+
+// from Function f, CmdCall c
+// where f.getLocation().getFile().getBaseName() = "sanitizers.ps1"
+// select f, f.getAParameter().getStaticType(), f.getAParameter().getName()
+
+
+//TBD, waiting on mathias on how to connect f and c
+// from Function f, CmdCall c, Parameter p, Argument a
+// where
+// p = f.getAParameter() and
+// a = c.getAnArgument() and
+// p.getName().toLowerCase() = a.getName() and
+// p.getStaticType() != "Object" and
+// c.getName() = f.getName()
+// select a, "argument has a specified static type"
+
+// from Argument a, VarReadAccess v
+// where a.getAChild() = v and
+// v.getVariable().getName() = "UserInput"
+// select a, v
+
+// from Argument e
+// where e.getLocation().getFile().getBaseName() = "sanitizers.ps1"
+// and e.getLocation().getStartLine() = 14
+// select e, e.getAChild(), e.getParent(), e.toString()
+
+
+from Parameter p
+where p.getLocation().getFile().getBaseName() = "userinput.ps1"
+// p.getAnAttribute().toString() = "ValueFromPipeline" and
+
+select p, p.getName()
+
+// from Expr e
+// where e.getLocation().getFile().getBaseName() = "userinput.ps1"
+// select e, e.getAQlClass()
+
+// from InvokeMemberExpr ie
+// where
+// ie.getLocation().getStartLine() = 28 and ie.getName() = "AddScript"
+// select ie, ie.getName(), ie.getQualifier().toString(), ie.getQualifier().getAChild().toString(), ie.getParent().(InvokeMemberExpr).getName()
\ No newline at end of file
From 38f0f07d57bdc08c534806fc1cf7fef852a4ec98 Mon Sep 17 00:00:00 2001
From: Chanel Young
Date: Fri, 4 Apr 2025 09:03:39 -0700
Subject: [PATCH 2/6] modeled some user input, sanitizers
---
.../UserInputToDangerousMethod.ql | 59 +++++++++----------
1 file changed, 29 insertions(+), 30 deletions(-)
diff --git a/powershell/ql/src/experimental/InjectionHunter/UserInputToDangerousMethod.ql b/powershell/ql/src/experimental/InjectionHunter/UserInputToDangerousMethod.ql
index bdbcdbddfe3..aaa450815de 100644
--- a/powershell/ql/src/experimental/InjectionHunter/UserInputToDangerousMethod.ql
+++ b/powershell/ql/src/experimental/InjectionHunter/UserInputToDangerousMethod.ql
@@ -22,7 +22,7 @@ private module TestConfig implements DataFlow::ConfigSig {
c.getName() = "Read-Host" and
source.asExpr().getExpr() = c) }
- predicate isSink(DataFlow::Node sink) { sink instanceof Sink }
+ predicate isSink(DataFlow::Node sink) { any()}//sink instanceof Sink }
predicate isBarrier(DataFlow::Node node) {node instanceof Sanitizer}
}
@@ -98,37 +98,33 @@ class AddScriptInvokeSink extends Sink {
abstract class Sanitizer extends DataFlow::Node {}
-// class TypedParameterSanitizer extends Sanitizer{
-// TypedParameterSanitizer() {
-// exists(Function f, CmdCall c, Parameter p, Argument a |
-// p = f.getAParameter() and
-// a = c.getAnArgument() and
-// p.getName().toLowerCase() = a.getName() and
-// p.getStaticType() != "Object" and
-// c.getName() = f.getName() and
-
-// this.asExpr().getExpr() = a
+// class TypedParameterSanitizer extends Sanitizer {
+// TypedParameterSanitizer() {
+// exists(Function f, Parameter p |
+// p = f.getAParameter() and
+// p.getStaticType() != "Object" and
+// this.asParameter() = p
+// )
+// }
+// }
+
+// class SingleQuoteSanitizer extends Sanitizer {
+// SingleQuoteSanitizer() {
+// exists(Expr e, VarReadAccess v |
+// e = this.asExpr().getExpr().getParent() and
+// e.toString().matches("%'$" + v.getVariable().getName() + "'%")
// )
// }
// }
-class SingleQuoteSanitizer extends Sanitizer {
- SingleQuoteSanitizer() {
- exists(Expr e, VarReadAccess v |
- e = this.asExpr().getExpr().getParent() and
- e.toString().matches("%'$" + v.getVariable().getName() + "'%")
- )
- }
-}
-
module TestFlow = TaintTracking::Global;
import TestFlow::PathGraph
-// from TestFlow::PathNode source, TestFlow::PathNode sink
-// where
-// TestFlow::flowPath(source, sink) and
-// sink.getNode().asExpr().getExpr().getLocation().getFile().getBaseName() = "sanitizers.ps1"
-// select sink.getNode(), source, sink, "Flow from user input to Invoke-Expression"
+from TestFlow::PathNode source, TestFlow::PathNode sink
+where
+ TestFlow::flowPath(source, sink) and
+ sink.getNode().asExpr().getExpr().getLocation().getFile().getBaseName() = "sanitizers.ps1"
+select sink.getNode(), source, sink, "Flow from user input to Invoke-Expression"
// from Function f, CmdCall c
// where f.getLocation().getFile().getBaseName() = "sanitizers.ps1"
@@ -155,15 +151,18 @@ import TestFlow::PathGraph
// and e.getLocation().getStartLine() = 14
// select e, e.getAChild(), e.getParent(), e.toString()
+// from PipelineParameter p
+// where p.getLocation().getFile().getBaseName() = "userinput.ps1"
+// select p, p.getName(), p.getAChild()
+
+// from Attribute a
+// select a, a.getParent(), a.getParent().getAQlClass(), a.getANamedArgument()
-from Parameter p
-where p.getLocation().getFile().getBaseName() = "userinput.ps1"
-// p.getAnAttribute().toString() = "ValueFromPipeline" and
-select p, p.getName()
// from Expr e
-// where e.getLocation().getFile().getBaseName() = "userinput.ps1"
+// where e.getLocation().getFile().getBaseName() = "sanitizers.ps1"
+// and e.getLocation().getStartLine() = 31
// select e, e.getAQlClass()
// from InvokeMemberExpr ie
From 5f643509f01cd1a7ca83d02862f600b5d0ba0e18 Mon Sep 17 00:00:00 2001
From: Chanel Young
Date: Wed, 16 Apr 2025 11:18:02 -0700
Subject: [PATCH 3/6] added script block, expandstring sinks, moved sanitizers
to separate file
---
.../InjectionHunter/Sanitizers.qll | 26 ++++
.../InjectionHunter/UserInput.qll | 0
.../UserInputToDangerousMethod.ql | 142 +++++++++++++-----
3 files changed, 132 insertions(+), 36 deletions(-)
create mode 100644 powershell/ql/src/experimental/InjectionHunter/Sanitizers.qll
delete mode 100644 powershell/ql/src/experimental/InjectionHunter/UserInput.qll
diff --git a/powershell/ql/src/experimental/InjectionHunter/Sanitizers.qll b/powershell/ql/src/experimental/InjectionHunter/Sanitizers.qll
new file mode 100644
index 00000000000..ac635928e10
--- /dev/null
+++ b/powershell/ql/src/experimental/InjectionHunter/Sanitizers.qll
@@ -0,0 +1,26 @@
+import powershell
+import semmle.code.powershell.dataflow.TaintTracking
+import semmle.code.powershell.dataflow.DataFlow
+import semmle.code.powershell.ApiGraphs
+
+
+abstract class Sanitizer extends DataFlow::Node {}
+
+class TypedParameterSanitizer extends Sanitizer {
+ TypedParameterSanitizer() {
+ exists(Function f, Parameter p |
+ p = f.getAParameter() and
+ p.getStaticType() != "Object" and
+ this.asParameter() = p
+ )
+ }
+}
+
+class SingleQuoteSanitizer extends Sanitizer {
+ SingleQuoteSanitizer() {
+ exists(Expr e, VarReadAccess v |
+ e = this.asExpr().getExpr().getParent() and
+ e.toString().matches("%'$" + v.getVariable().getName() + "'%")
+ )
+ }
+}
diff --git a/powershell/ql/src/experimental/InjectionHunter/UserInput.qll b/powershell/ql/src/experimental/InjectionHunter/UserInput.qll
deleted file mode 100644
index e69de29bb2d..00000000000
diff --git a/powershell/ql/src/experimental/InjectionHunter/UserInputToDangerousMethod.ql b/powershell/ql/src/experimental/InjectionHunter/UserInputToDangerousMethod.ql
index aaa450815de..75a0e3aab12 100644
--- a/powershell/ql/src/experimental/InjectionHunter/UserInputToDangerousMethod.ql
+++ b/powershell/ql/src/experimental/InjectionHunter/UserInputToDangerousMethod.ql
@@ -1,6 +1,6 @@
/**
* @name User Input to Invoke-Expression
- * @description Finding cases where the user input is passed an Invoke-Expression command
+ * @description Finding cases where the user input is passed an dangerous method that can lead to RCE
* @kind path-problem
* @problem.severity error
* @security-severity 9.8
@@ -15,14 +15,17 @@ import powershell
import semmle.code.powershell.dataflow.TaintTracking
import semmle.code.powershell.dataflow.DataFlow
import semmle.code.powershell.ApiGraphs
+import semmle.code.powershell.dataflow.flowsources.FlowSources
+
+import Sanitizers
private module TestConfig implements DataFlow::ConfigSig {
predicate isSource(DataFlow::Node source) {
- exists(CmdCall c |
- c.getName() = "Read-Host" and
- source.asExpr().getExpr() = c) }
+ source instanceof SourceNode or
+ source instanceof Source
+ }
- predicate isSink(DataFlow::Node sink) { any()}//sink instanceof Sink }
+ predicate isSink(DataFlow::Node sink) { sink instanceof Sink }
predicate isBarrier(DataFlow::Node node) {node instanceof Sanitizer}
}
@@ -65,22 +68,19 @@ class InvokeExpressionCall extends Sink {
class InvokeScriptSink extends Sink {
InvokeScriptSink() {
- exists(InvokeMemberExpr ie |
- this.asExpr().getExpr() = ie.getAnArgument() and
- ie.getName() = "InvokeScript" and
- ie.getQualifier().toString() = "InvokeCommand" and
- ie.getQualifier().getAChild().toString() = "executioncontext"
+ exists(API::Node call |
+ API::getTopLevelMember("executioncontext").getMember("invokecommand").getMethod("invokescript") = call and
+ this = call.getArgument(_).asSink()
)
}
}
class CreateNestedPipelineSink extends Sink {
CreateNestedPipelineSink() {
- exists(InvokeMemberExpr ie |
- this.asExpr().getExpr() = ie.getAnArgument() and
- ie.getName() = "CreateNestedPipeline" and
- ie.getQualifier().toString() = "InvokeCommand" and
- ie.getQualifier().getAChild().toString() = "executioncontext")
+ exists(API::Node call |
+ API::getTopLevelMember("host").getMember("runspace").getMethod("createnestedpipeline") = call and
+ this = call.getArgument(_).asSink()
+ )
}
}
@@ -96,35 +96,105 @@ class AddScriptInvokeSink extends Sink {
}
}
-abstract class Sanitizer extends DataFlow::Node {}
+class PowershellSink extends Sink {
+ PowershellSink() {
+ exists( CmdCall c |
+ c.getName() = "powershell" |
+ (
+ this.asExpr().getExpr() = c.getArgument(1) and
+ c.getArgument(0).getValue().toString() = "-command"
+ ) or
+ (
+ this.asExpr().getExpr() = c.getArgument(0)
+ )
+ )
+ }
+}
-// class TypedParameterSanitizer extends Sanitizer {
-// TypedParameterSanitizer() {
-// exists(Function f, Parameter p |
-// p = f.getAParameter() and
-// p.getStaticType() != "Object" and
-// this.asParameter() = p
-// )
-// }
-// }
+class CmdSink extends Sink {
+ CmdSink() {
+ exists(CmdCall c |
+ this.asExpr().getExpr() = c.getArgument(1) and
+ c.getName() = "cmd" and
+ c.getArgument(0).getValue().toString() = "/c"
+ )
+ }
+}
-// class SingleQuoteSanitizer extends Sanitizer {
-// SingleQuoteSanitizer() {
-// exists(Expr e, VarReadAccess v |
-// e = this.asExpr().getExpr().getParent() and
-// e.toString().matches("%'$" + v.getVariable().getName() + "'%")
-// )
-// }
-// }
+class ForEachObjectSink extends Sink {
+ ForEachObjectSink() {
+ exists(CmdCall c |
+ this.asExpr().getExpr() = c.getAnArgument() and
+ c.getName() = "Foreach-Object"
+ )
+ }
+}
+
+class InvokeSink extends Sink {
+ InvokeSink() {
+ exists(InvokeMemberExpr ie |
+ this.asExpr().getExpr() = ie.getCallee() or
+ this.asExpr().getExpr() = ie.getQualifier().getAChild*()
+ )
+ }
+}
+
+class CreateScriptBlockSink extends Sink {
+ CreateScriptBlockSink() {
+ exists(InvokeMemberExpr ie |
+ this.asExpr().getExpr() = ie.getAnArgument() and
+ ie.getName() = "Create" and
+ ie.getQualifier().toString() = "ScriptBlock"
+ )
+ }
+}
+
+class NewScriptBlockSink extends Sink {
+ NewScriptBlockSink() {
+ exists(API::Node call |
+ API::getTopLevelMember("executioncontext").getMember("invokecommand").getMethod("newscriptblock") = call and
+ this = call.getArgument(_).asSink()
+ )
+ }
+}
+
+class ExpandStringSink extends Sink {
+ ExpandStringSink() {
+ exists(API::Node call | this = call.getArgument(_).asSink() |
+ API::getTopLevelMember("executioncontext").getMember("invokecommand").getMethod("expandstring") = call or
+ API::getTopLevelMember("executioncontext").getMember("sessionstate").getMember("invokecommand").getMethod("expandstring") = call
+
+ )
+ }
+}
module TestFlow = TaintTracking::Global;
import TestFlow::PathGraph
from TestFlow::PathNode source, TestFlow::PathNode sink
where
- TestFlow::flowPath(source, sink) and
- sink.getNode().asExpr().getExpr().getLocation().getFile().getBaseName() = "sanitizers.ps1"
-select sink.getNode(), source, sink, "Flow from user input to Invoke-Expression"
+ TestFlow::flowPath(source, sink)
+select sink.getNode(), source, sink, "Flow from user input to dangerous method"
+
+// from CmdCall c
+// where c.getName() = "cmd"
+// and c.getArgument(0).getValue().toString() = "/c"
+// select c.getArgument(1)
+
+// from InvokeMemberExpr ie
+// where ie.getName() = "Create" and
+// ie.getQualifier().toString() = "ScriptBlock"
+// select ie, ie.getQualifier(), ie.getAnArgument()
+
+// from API::Node call
+// where API::getTopLevelMember("executioncontext").getMember("invokecommand").getMethod("newscriptblock") = call
+// select call, call.getArgument(_).asSink()
+
+// from Expr e
+// where e.getLocation().getFile().getBaseName() = "InjectionHunterTests.ps1"
+// and e.getLocation().getStartLine() = 106
+// select e, e.getAQlClass()
+
// from Function f, CmdCall c
// where f.getLocation().getFile().getBaseName() = "sanitizers.ps1"
From 2266cd2eb8d7beb02736599a16f34296df628b52 Mon Sep 17 00:00:00 2001
From: Chanel Young
Date: Wed, 16 Apr 2025 12:13:07 -0700
Subject: [PATCH 4/6] moved folder, added tests/docs
---
.../UserInputToDangerousMethod.ql | 241 ------------------
.../cwe-078}/InjectionHunter/Sanitizers.qll | 0
.../cwe-078/InjectionHunter/Sinks.qll | 152 +++++++++++
.../UserInputToDangerousMethod.qhelp | 32 ++-
.../UserInputToDangerousMethod.ql | 36 +++
.../InjectionHunter/InjectionHunter.expected | 146 +++++++++++
.../InjectionHunter/InjectionHunter.qlref | 1 +
.../security/cwe-078/InjectionHunter/test.ps1 | 221 ++++++++++++++++
8 files changed, 576 insertions(+), 253 deletions(-)
delete mode 100644 powershell/ql/src/experimental/InjectionHunter/UserInputToDangerousMethod.ql
rename powershell/ql/src/{experimental => queries/security/cwe-078}/InjectionHunter/Sanitizers.qll (100%)
create mode 100644 powershell/ql/src/queries/security/cwe-078/InjectionHunter/Sinks.qll
rename powershell/ql/src/{experimental => queries/security/cwe-078}/InjectionHunter/UserInputToDangerousMethod.qhelp (52%)
create mode 100644 powershell/ql/src/queries/security/cwe-078/InjectionHunter/UserInputToDangerousMethod.ql
create mode 100644 powershell/ql/test/query-tests/security/cwe-078/InjectionHunter/InjectionHunter.expected
create mode 100644 powershell/ql/test/query-tests/security/cwe-078/InjectionHunter/InjectionHunter.qlref
create mode 100644 powershell/ql/test/query-tests/security/cwe-078/InjectionHunter/test.ps1
diff --git a/powershell/ql/src/experimental/InjectionHunter/UserInputToDangerousMethod.ql b/powershell/ql/src/experimental/InjectionHunter/UserInputToDangerousMethod.ql
deleted file mode 100644
index 75a0e3aab12..00000000000
--- a/powershell/ql/src/experimental/InjectionHunter/UserInputToDangerousMethod.ql
+++ /dev/null
@@ -1,241 +0,0 @@
-/**
- * @name User Input to Invoke-Expression
- * @description Finding cases where the user input is passed an dangerous method that can lead to RCE
- * @kind path-problem
- * @problem.severity error
- * @security-severity 9.8
- * @precision high
- * @id powershell/microsoft/public/user-input-to-invoke-expression
- * @tags security
- * external/cwe/cwe-078
- * external/cwe/cwe-088
- */
-
-import powershell
-import semmle.code.powershell.dataflow.TaintTracking
-import semmle.code.powershell.dataflow.DataFlow
-import semmle.code.powershell.ApiGraphs
-import semmle.code.powershell.dataflow.flowsources.FlowSources
-
-import Sanitizers
-
-private module TestConfig implements DataFlow::ConfigSig {
- predicate isSource(DataFlow::Node source) {
- source instanceof SourceNode or
- source instanceof Source
- }
-
- predicate isSink(DataFlow::Node sink) { sink instanceof Sink }
- predicate isBarrier(DataFlow::Node node) {node instanceof Sanitizer}
-}
-
-abstract class Source extends DataFlow::Node {}
-
-class ReadHostSource extends Source {
- ReadHostSource() {
- exists(CmdCall c |
- this.asExpr().getExpr() = c and
- c.getName() = "Read-Host" )
- }
-}
-
-class GetContentSource extends Source {
- GetContentSource() {
- exists(CmdCall c |
- this.asExpr().getExpr() = c and
- c.getName() = "Get-Content" )
- }
-}
-
-class ValueFromPipelineSource extends Source {
- ValueFromPipelineSource() {
- exists(Parameter p |
- p.getAnAttribute().toString() = "ValueFromPipeline" and
- this.asExpr().getExpr() = p.getAnAccess()
- )
- }
-}
-
-abstract class Sink extends DataFlow::Node {}
-
-class InvokeExpressionCall extends Sink {
- InvokeExpressionCall() {
- exists(CmdCall c |
- this.asExpr().getExpr() = c.getAnArgument() and
- c.getName() = ["Invoke-Expression", "iex", "Add-Type" ] )
- }
-}
-
-class InvokeScriptSink extends Sink {
- InvokeScriptSink() {
- exists(API::Node call |
- API::getTopLevelMember("executioncontext").getMember("invokecommand").getMethod("invokescript") = call and
- this = call.getArgument(_).asSink()
- )
- }
-}
-
-class CreateNestedPipelineSink extends Sink {
- CreateNestedPipelineSink() {
- exists(API::Node call |
- API::getTopLevelMember("host").getMember("runspace").getMethod("createnestedpipeline") = call and
- this = call.getArgument(_).asSink()
- )
- }
-}
-
-class AddScriptInvokeSink extends Sink {
- AddScriptInvokeSink() {
- exists(InvokeMemberExpr ie |
- this.asExpr().getExpr() = ie.getAnArgument() and
- ie.getName() = "AddScript" and
- ie.getQualifier().(InvokeMemberExpr).getName() = "Create" and
- ie.getQualifier().getAChild().toString() = "PowerShell" and
- ie.getParent().(InvokeMemberExpr).getName() = "Invoke"
- )
- }
-}
-
-class PowershellSink extends Sink {
- PowershellSink() {
- exists( CmdCall c |
- c.getName() = "powershell" |
- (
- this.asExpr().getExpr() = c.getArgument(1) and
- c.getArgument(0).getValue().toString() = "-command"
- ) or
- (
- this.asExpr().getExpr() = c.getArgument(0)
- )
- )
- }
-}
-
-class CmdSink extends Sink {
- CmdSink() {
- exists(CmdCall c |
- this.asExpr().getExpr() = c.getArgument(1) and
- c.getName() = "cmd" and
- c.getArgument(0).getValue().toString() = "/c"
- )
- }
-}
-
-class ForEachObjectSink extends Sink {
- ForEachObjectSink() {
- exists(CmdCall c |
- this.asExpr().getExpr() = c.getAnArgument() and
- c.getName() = "Foreach-Object"
- )
- }
-}
-
-class InvokeSink extends Sink {
- InvokeSink() {
- exists(InvokeMemberExpr ie |
- this.asExpr().getExpr() = ie.getCallee() or
- this.asExpr().getExpr() = ie.getQualifier().getAChild*()
- )
- }
-}
-
-class CreateScriptBlockSink extends Sink {
- CreateScriptBlockSink() {
- exists(InvokeMemberExpr ie |
- this.asExpr().getExpr() = ie.getAnArgument() and
- ie.getName() = "Create" and
- ie.getQualifier().toString() = "ScriptBlock"
- )
- }
-}
-
-class NewScriptBlockSink extends Sink {
- NewScriptBlockSink() {
- exists(API::Node call |
- API::getTopLevelMember("executioncontext").getMember("invokecommand").getMethod("newscriptblock") = call and
- this = call.getArgument(_).asSink()
- )
- }
-}
-
-class ExpandStringSink extends Sink {
- ExpandStringSink() {
- exists(API::Node call | this = call.getArgument(_).asSink() |
- API::getTopLevelMember("executioncontext").getMember("invokecommand").getMethod("expandstring") = call or
- API::getTopLevelMember("executioncontext").getMember("sessionstate").getMember("invokecommand").getMethod("expandstring") = call
-
- )
- }
-}
-
-module TestFlow = TaintTracking::Global;
-import TestFlow::PathGraph
-
-from TestFlow::PathNode source, TestFlow::PathNode sink
-where
- TestFlow::flowPath(source, sink)
-select sink.getNode(), source, sink, "Flow from user input to dangerous method"
-
-// from CmdCall c
-// where c.getName() = "cmd"
-// and c.getArgument(0).getValue().toString() = "/c"
-// select c.getArgument(1)
-
-// from InvokeMemberExpr ie
-// where ie.getName() = "Create" and
-// ie.getQualifier().toString() = "ScriptBlock"
-// select ie, ie.getQualifier(), ie.getAnArgument()
-
-// from API::Node call
-// where API::getTopLevelMember("executioncontext").getMember("invokecommand").getMethod("newscriptblock") = call
-// select call, call.getArgument(_).asSink()
-
-// from Expr e
-// where e.getLocation().getFile().getBaseName() = "InjectionHunterTests.ps1"
-// and e.getLocation().getStartLine() = 106
-// select e, e.getAQlClass()
-
-
-// from Function f, CmdCall c
-// where f.getLocation().getFile().getBaseName() = "sanitizers.ps1"
-// select f, f.getAParameter().getStaticType(), f.getAParameter().getName()
-
-
-//TBD, waiting on mathias on how to connect f and c
-// from Function f, CmdCall c, Parameter p, Argument a
-// where
-// p = f.getAParameter() and
-// a = c.getAnArgument() and
-// p.getName().toLowerCase() = a.getName() and
-// p.getStaticType() != "Object" and
-// c.getName() = f.getName()
-// select a, "argument has a specified static type"
-
-// from Argument a, VarReadAccess v
-// where a.getAChild() = v and
-// v.getVariable().getName() = "UserInput"
-// select a, v
-
-// from Argument e
-// where e.getLocation().getFile().getBaseName() = "sanitizers.ps1"
-// and e.getLocation().getStartLine() = 14
-// select e, e.getAChild(), e.getParent(), e.toString()
-
-// from PipelineParameter p
-// where p.getLocation().getFile().getBaseName() = "userinput.ps1"
-// select p, p.getName(), p.getAChild()
-
-// from Attribute a
-// select a, a.getParent(), a.getParent().getAQlClass(), a.getANamedArgument()
-
-
-
-// from Expr e
-// where e.getLocation().getFile().getBaseName() = "sanitizers.ps1"
-// and e.getLocation().getStartLine() = 31
-// select e, e.getAQlClass()
-
-// from InvokeMemberExpr ie
-// where
-// ie.getLocation().getStartLine() = 28 and ie.getName() = "AddScript"
-// select ie, ie.getName(), ie.getQualifier().toString(), ie.getQualifier().getAChild().toString(), ie.getParent().(InvokeMemberExpr).getName()
\ No newline at end of file
diff --git a/powershell/ql/src/experimental/InjectionHunter/Sanitizers.qll b/powershell/ql/src/queries/security/cwe-078/InjectionHunter/Sanitizers.qll
similarity index 100%
rename from powershell/ql/src/experimental/InjectionHunter/Sanitizers.qll
rename to powershell/ql/src/queries/security/cwe-078/InjectionHunter/Sanitizers.qll
diff --git a/powershell/ql/src/queries/security/cwe-078/InjectionHunter/Sinks.qll b/powershell/ql/src/queries/security/cwe-078/InjectionHunter/Sinks.qll
new file mode 100644
index 00000000000..4c62966746f
--- /dev/null
+++ b/powershell/ql/src/queries/security/cwe-078/InjectionHunter/Sinks.qll
@@ -0,0 +1,152 @@
+import powershell
+import semmle.code.powershell.dataflow.TaintTracking
+import semmle.code.powershell.dataflow.DataFlow
+import semmle.code.powershell.ApiGraphs
+import semmle.code.powershell.dataflow.flowsources.FlowSources
+
+abstract class InjectionSink extends DataFlow::Node {
+ abstract string getSinkType();
+}
+
+class InvokeExpressionCall extends InjectionSink {
+ InvokeExpressionCall() {
+ exists(CmdCall c |
+ this.asExpr().getExpr() = c.getAnArgument() and
+ c.getName() = ["Invoke-Expression", "iex", "Add-Type" ] )
+ }
+ override string getSinkType(){
+ result = "call to Invoke-Expression"
+ }
+}
+
+class InvokeScriptSink extends InjectionSink {
+ InvokeScriptSink() {
+ exists(API::Node call |
+ API::getTopLevelMember("executioncontext").getMember("invokecommand").getMethod("invokescript") = call and
+ this = call.getArgument(_).asSink()
+ )
+ }
+ override string getSinkType(){
+ result = "call to InvokeScript"
+ }
+}
+
+class CreateNestedPipelineSink extends InjectionSink {
+ CreateNestedPipelineSink() {
+ exists(API::Node call |
+ API::getTopLevelMember("host").getMember("runspace").getMethod("createnestedpipeline") = call and
+ this = call.getArgument(_).asSink()
+ )
+ }
+ override string getSinkType(){
+ result = "call to CreateNestedPipeline"
+ }
+}
+
+class AddScriptInvokeSink extends InjectionSink {
+ AddScriptInvokeSink() {
+ exists(InvokeMemberExpr ie |
+ this.asExpr().getExpr() = ie.getAnArgument() and
+ ie.getName() = "AddScript" and
+ ie.getQualifier().(InvokeMemberExpr).getName() = "Create" and
+ ie.getQualifier().getAChild().toString() = "PowerShell" and
+ ie.getParent().(InvokeMemberExpr).getName() = "Invoke"
+ )
+ }
+ override string getSinkType(){
+ result = "call to AddScript"
+ }
+}
+
+class PowershellSink extends InjectionSink {
+ PowershellSink() {
+ exists( CmdCall c |
+ c.getName() = "powershell" |
+ (
+ this.asExpr().getExpr() = c.getArgument(1) and
+ c.getArgument(0).getValue().toString() = "-command"
+ ) or
+ (
+ this.asExpr().getExpr() = c.getArgument(0)
+ )
+ )
+ }
+ override string getSinkType(){
+ result = "call to Powershell"
+ }
+}
+
+class CmdSink extends InjectionSink {
+ CmdSink() {
+ exists(CmdCall c |
+ this.asExpr().getExpr() = c.getArgument(1) and
+ c.getName() = "cmd" and
+ c.getArgument(0).getValue().toString() = "/c"
+ )
+ }
+ override string getSinkType(){
+ result = "call to Cmd"
+ }
+}
+
+class ForEachObjectSink extends InjectionSink {
+ ForEachObjectSink() {
+ exists(CmdCall c |
+ this.asExpr().getExpr() = c.getAnArgument() and
+ c.getName() = "Foreach-Object"
+ )
+ }
+ override string getSinkType(){
+ result = "call to ForEach-Object"
+ }
+}
+
+class InvokeSink extends InjectionSink {
+ InvokeSink() {
+ exists(InvokeMemberExpr ie |
+ this.asExpr().getExpr() = ie.getCallee() or
+ this.asExpr().getExpr() = ie.getQualifier().getAChild*()
+ )
+ }
+ override string getSinkType(){
+ result = "call to Invoke"
+ }
+}
+
+class CreateScriptBlockSink extends InjectionSink {
+ CreateScriptBlockSink() {
+ exists(InvokeMemberExpr ie |
+ this.asExpr().getExpr() = ie.getAnArgument() and
+ ie.getName() = "Create" and
+ ie.getQualifier().toString() = "ScriptBlock"
+ )
+ }
+ override string getSinkType(){
+ result = "call to CreateScriptBlock"
+ }
+}
+
+class NewScriptBlockSink extends InjectionSink {
+ NewScriptBlockSink() {
+ exists(API::Node call |
+ API::getTopLevelMember("executioncontext").getMember("invokecommand").getMethod("newscriptblock") = call and
+ this = call.getArgument(_).asSink()
+ )
+ }
+ override string getSinkType(){
+ result = "call to NewScriptBlock"
+ }
+}
+
+class ExpandStringSink extends InjectionSink {
+ ExpandStringSink() {
+ exists(API::Node call | this = call.getArgument(_).asSink() |
+ API::getTopLevelMember("executioncontext").getMember("invokecommand").getMethod("expandstring") = call or
+ API::getTopLevelMember("executioncontext").getMember("sessionstate").getMember("invokecommand").getMethod("expandstring") = call
+
+ )
+ }
+ override string getSinkType(){
+ result = "call to ExpandString"
+ }
+}
\ No newline at end of file
diff --git a/powershell/ql/src/experimental/InjectionHunter/UserInputToDangerousMethod.qhelp b/powershell/ql/src/queries/security/cwe-078/InjectionHunter/UserInputToDangerousMethod.qhelp
similarity index 52%
rename from powershell/ql/src/experimental/InjectionHunter/UserInputToDangerousMethod.qhelp
rename to powershell/ql/src/queries/security/cwe-078/InjectionHunter/UserInputToDangerousMethod.qhelp
index f9ffbe53403..de459c2e84f 100644
--- a/powershell/ql/src/experimental/InjectionHunter/UserInputToDangerousMethod.qhelp
+++ b/powershell/ql/src/queries/security/cwe-078/InjectionHunter/UserInputToDangerousMethod.qhelp
@@ -8,33 +8,41 @@
routine that executes a command, allows the user to execute malicious
code.
+This is a port of the InjectionHunter tool by Lee Holmes, and checks when user input is passed to any of the following:
+
+ - Invoke-Expression
+ - InvokeScript
+ - CreateNestedPipeline
+ - AddScript
+ - powershell
+ - cmd
+ - Foreach-Object
+ - Invoke
+ - CreateScriptBlock
+ - NewScriptBlock
+ - ExpandString
+
+
-Possible script injection risk via the Invoke-Expression cmdlet. Untrusted input can cause arbitrary PowerShell expressions to be run.
+
Possible script injection risk. Untrusted input can cause arbitrary PowerShell expressions to be run.
Variables may be used directly for dynamic parameter arguments, splatting can be used for dynamic parameter names,
and the invocation operator can be used for dynamic command names. If content escaping is truly needed, PowerShell has several valid quote characters,
so [System.Management.Automation.Language.CodeGeneration]::Escape* should be used.
-
-The following example shows code that takes a shell script that can be changed
-maliciously by a user, and passes it straight to Invoke-Expression
-without examining it first.
-
-
-
-
OWASP:
Command Injection.
-
-
+
+Injection Hunter:
+PowerShell Injection Hunter: Security Auditing for PowerShell Scripts.
+
diff --git a/powershell/ql/src/queries/security/cwe-078/InjectionHunter/UserInputToDangerousMethod.ql b/powershell/ql/src/queries/security/cwe-078/InjectionHunter/UserInputToDangerousMethod.ql
new file mode 100644
index 00000000000..11730a65299
--- /dev/null
+++ b/powershell/ql/src/queries/security/cwe-078/InjectionHunter/UserInputToDangerousMethod.ql
@@ -0,0 +1,36 @@
+/**
+ * @name User Input to injection sink
+ * @description Finding cases where the user input is passed an dangerous method that can lead to RCE
+ * @kind path-problem
+ * @problem.severity error
+ * @security-severity 9.8
+ * @precision high
+ * @id powershell/microsoft/public/user-input-to-injection-sink
+ * @tags security
+ * external/cwe/cwe-078
+ * external/cwe/cwe-088
+ */
+
+import powershell
+import semmle.code.powershell.dataflow.TaintTracking
+import semmle.code.powershell.dataflow.DataFlow
+import semmle.code.powershell.ApiGraphs
+import semmle.code.powershell.dataflow.flowsources.FlowSources
+
+import Sanitizers
+import Sinks
+
+private module InjectionConfig implements DataFlow::ConfigSig {
+ predicate isSource(DataFlow::Node source) {
+ source instanceof SourceNode
+ }
+ predicate isSink(DataFlow::Node sink) { sink instanceof InjectionSink }
+ predicate isBarrier(DataFlow::Node node) {node instanceof Sanitizer}
+}
+
+module InjectionFlow = TaintTracking::Global;
+import InjectionFlow::PathGraph
+
+from InjectionFlow::PathNode source, InjectionFlow::PathNode sink
+where InjectionFlow::flowPath(source, sink)
+select sink.getNode(), source, sink, "Possible injection path from user input to dangerous " + sink.getNode().(InjectionSink).getSinkType()
diff --git a/powershell/ql/test/query-tests/security/cwe-078/InjectionHunter/InjectionHunter.expected b/powershell/ql/test/query-tests/security/cwe-078/InjectionHunter/InjectionHunter.expected
new file mode 100644
index 00000000000..8bfcc1dafec
--- /dev/null
+++ b/powershell/ql/test/query-tests/security/cwe-078/InjectionHunter/InjectionHunter.expected
@@ -0,0 +1,146 @@
+edges
+| test.ps1:3:11:3:20 | UserInput | test.ps1:4:23:4:52 | Get-Process -Name $UserInput | provenance | |
+| test.ps1:9:11:9:20 | UserInput | test.ps1:10:9:10:38 | Get-Process -Name $UserInput | provenance | |
+| test.ps1:15:11:15:20 | UserInput | test.ps1:16:50:16:79 | Get-Process -Name $UserInput | provenance | |
+| test.ps1:21:11:21:20 | UserInput | test.ps1:22:41:22:70 | Get-Process -Name $UserInput | provenance | |
+| test.ps1:21:11:21:20 | UserInput | test.ps1:22:60:22:69 | UserInput | provenance | |
+| test.ps1:27:11:27:20 | UserInput | test.ps1:28:38:28:67 | Get-Process -Name $UserInput | provenance | |
+| test.ps1:27:11:27:20 | UserInput | test.ps1:28:57:28:66 | UserInput | provenance | |
+| test.ps1:33:11:33:20 | UserInput | test.ps1:34:14:34:46 | public class Foo { $UserInput } | provenance | |
+| test.ps1:39:11:39:20 | UserInput | test.ps1:40:30:40:62 | public class Foo { $UserInput } | provenance | |
+| test.ps1:45:11:45:20 | UserInput | test.ps1:48:30:48:34 | code | provenance | |
+| test.ps1:73:11:73:20 | UserInput | test.ps1:75:25:75:54 | Get-Process -Name $UserInput | provenance | |
+| test.ps1:80:11:80:20 | UserInput | test.ps1:82:16:82:45 | Get-Process -Name $UserInput | provenance | |
+| test.ps1:87:11:87:20 | UserInput | test.ps1:89:12:89:28 | ping $UserInput | provenance | |
+| test.ps1:102:11:102:20 | UserInput | test.ps1:106:33:106:62 | Get-Process -Name $UserInput | provenance | |
+| test.ps1:112:11:112:20 | UserInput | test.ps1:116:58:116:87 | Get-Process -Name $UserInput | provenance | |
+| test.ps1:122:11:122:20 | UserInput | test.ps1:124:34:124:43 | UserInput | provenance | |
+| test.ps1:129:11:129:20 | UserInput | test.ps1:131:28:131:37 | UserInput | provenance | |
+| test.ps1:136:11:136:20 | UserInput | test.ps1:138:28:138:37 | UserInput | provenance | |
+| test.ps1:165:11:165:20 | UserInput | test.ps1:168:50:168:59 | UserInput | provenance | |
+| test.ps1:173:11:173:20 | UserInput | test.ps1:176:63:176:72 | UserInput | provenance | |
+| test.ps1:189:11:189:20 | UserInput | test.ps1:192:23:192:54 | Get-Process -Name "$escaped" | provenance | |
+| test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:197:46:197:51 | input | provenance | Src:MaD:11464 |
+| test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:198:46:198:51 | input | provenance | Src:MaD:11464 |
+| test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:199:46:199:51 | input | provenance | Src:MaD:11464 |
+| test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:200:46:200:51 | input | provenance | Src:MaD:11464 |
+| test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:201:46:201:51 | input | provenance | Src:MaD:11464 |
+| test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:202:46:202:51 | input | provenance | Src:MaD:11464 |
+| test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:203:46:203:51 | input | provenance | Src:MaD:11464 |
+| test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:204:46:204:51 | input | provenance | Src:MaD:11464 |
+| test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:206:48:206:53 | input | provenance | Src:MaD:11464 |
+| test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:207:48:207:53 | input | provenance | Src:MaD:11464 |
+| test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:208:48:208:53 | input | provenance | Src:MaD:11464 |
+| test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:210:41:210:46 | input | provenance | Src:MaD:11464 |
+| test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:211:41:211:46 | input | provenance | Src:MaD:11464 |
+| test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:212:36:212:41 | input | provenance | Src:MaD:11464 |
+| test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:213:36:213:41 | input | provenance | Src:MaD:11464 |
+| test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:214:36:214:41 | input | provenance | Src:MaD:11464 |
+| test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:218:42:218:47 | input | provenance | Src:MaD:11464 |
+| test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:219:42:219:47 | input | provenance | Src:MaD:11464 |
+| test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:221:33:221:38 | input | provenance | Src:MaD:11464 |
+| test.ps1:197:46:197:51 | input | test.ps1:3:11:3:20 | UserInput | provenance | |
+| test.ps1:198:46:198:51 | input | test.ps1:9:11:9:20 | UserInput | provenance | |
+| test.ps1:199:46:199:51 | input | test.ps1:15:11:15:20 | UserInput | provenance | |
+| test.ps1:200:46:200:51 | input | test.ps1:21:11:21:20 | UserInput | provenance | |
+| test.ps1:201:46:201:51 | input | test.ps1:27:11:27:20 | UserInput | provenance | |
+| test.ps1:202:46:202:51 | input | test.ps1:33:11:33:20 | UserInput | provenance | |
+| test.ps1:203:46:203:51 | input | test.ps1:39:11:39:20 | UserInput | provenance | |
+| test.ps1:204:46:204:51 | input | test.ps1:45:11:45:20 | UserInput | provenance | |
+| test.ps1:206:48:206:53 | input | test.ps1:73:11:73:20 | UserInput | provenance | |
+| test.ps1:207:48:207:53 | input | test.ps1:80:11:80:20 | UserInput | provenance | |
+| test.ps1:208:48:208:53 | input | test.ps1:87:11:87:20 | UserInput | provenance | |
+| test.ps1:210:41:210:46 | input | test.ps1:102:11:102:20 | UserInput | provenance | |
+| test.ps1:211:41:211:46 | input | test.ps1:112:11:112:20 | UserInput | provenance | |
+| test.ps1:212:36:212:41 | input | test.ps1:122:11:122:20 | UserInput | provenance | |
+| test.ps1:213:36:213:41 | input | test.ps1:129:11:129:20 | UserInput | provenance | |
+| test.ps1:214:36:214:41 | input | test.ps1:136:11:136:20 | UserInput | provenance | |
+| test.ps1:218:42:218:47 | input | test.ps1:165:11:165:20 | UserInput | provenance | |
+| test.ps1:219:42:219:47 | input | test.ps1:173:11:173:20 | UserInput | provenance | |
+| test.ps1:221:33:221:38 | input | test.ps1:189:11:189:20 | UserInput | provenance | |
+nodes
+| test.ps1:3:11:3:20 | UserInput | semmle.label | UserInput |
+| test.ps1:4:23:4:52 | Get-Process -Name $UserInput | semmle.label | Get-Process -Name $UserInput |
+| test.ps1:9:11:9:20 | UserInput | semmle.label | UserInput |
+| test.ps1:10:9:10:38 | Get-Process -Name $UserInput | semmle.label | Get-Process -Name $UserInput |
+| test.ps1:15:11:15:20 | UserInput | semmle.label | UserInput |
+| test.ps1:16:50:16:79 | Get-Process -Name $UserInput | semmle.label | Get-Process -Name $UserInput |
+| test.ps1:21:11:21:20 | UserInput | semmle.label | UserInput |
+| test.ps1:22:41:22:70 | Get-Process -Name $UserInput | semmle.label | Get-Process -Name $UserInput |
+| test.ps1:22:60:22:69 | UserInput | semmle.label | UserInput |
+| test.ps1:27:11:27:20 | UserInput | semmle.label | UserInput |
+| test.ps1:28:38:28:67 | Get-Process -Name $UserInput | semmle.label | Get-Process -Name $UserInput |
+| test.ps1:28:57:28:66 | UserInput | semmle.label | UserInput |
+| test.ps1:33:11:33:20 | UserInput | semmle.label | UserInput |
+| test.ps1:34:14:34:46 | public class Foo { $UserInput } | semmle.label | public class Foo { $UserInput } |
+| test.ps1:39:11:39:20 | UserInput | semmle.label | UserInput |
+| test.ps1:40:30:40:62 | public class Foo { $UserInput } | semmle.label | public class Foo { $UserInput } |
+| test.ps1:45:11:45:20 | UserInput | semmle.label | UserInput |
+| test.ps1:48:30:48:34 | code | semmle.label | code |
+| test.ps1:73:11:73:20 | UserInput | semmle.label | UserInput |
+| test.ps1:75:25:75:54 | Get-Process -Name $UserInput | semmle.label | Get-Process -Name $UserInput |
+| test.ps1:80:11:80:20 | UserInput | semmle.label | UserInput |
+| test.ps1:82:16:82:45 | Get-Process -Name $UserInput | semmle.label | Get-Process -Name $UserInput |
+| test.ps1:87:11:87:20 | UserInput | semmle.label | UserInput |
+| test.ps1:89:12:89:28 | ping $UserInput | semmle.label | ping $UserInput |
+| test.ps1:102:11:102:20 | UserInput | semmle.label | UserInput |
+| test.ps1:106:33:106:62 | Get-Process -Name $UserInput | semmle.label | Get-Process -Name $UserInput |
+| test.ps1:112:11:112:20 | UserInput | semmle.label | UserInput |
+| test.ps1:116:58:116:87 | Get-Process -Name $UserInput | semmle.label | Get-Process -Name $UserInput |
+| test.ps1:122:11:122:20 | UserInput | semmle.label | UserInput |
+| test.ps1:124:34:124:43 | UserInput | semmle.label | UserInput |
+| test.ps1:129:11:129:20 | UserInput | semmle.label | UserInput |
+| test.ps1:131:28:131:37 | UserInput | semmle.label | UserInput |
+| test.ps1:136:11:136:20 | UserInput | semmle.label | UserInput |
+| test.ps1:138:28:138:37 | UserInput | semmle.label | UserInput |
+| test.ps1:165:11:165:20 | UserInput | semmle.label | UserInput |
+| test.ps1:168:50:168:59 | UserInput | semmle.label | UserInput |
+| test.ps1:173:11:173:20 | UserInput | semmle.label | UserInput |
+| test.ps1:176:63:176:72 | UserInput | semmle.label | UserInput |
+| test.ps1:189:11:189:20 | UserInput | semmle.label | UserInput |
+| test.ps1:192:23:192:54 | Get-Process -Name "$escaped" | semmle.label | Get-Process -Name "$escaped" |
+| test.ps1:195:10:195:32 | Call to Read-Host | semmle.label | Call to Read-Host |
+| test.ps1:197:46:197:51 | input | semmle.label | input |
+| test.ps1:198:46:198:51 | input | semmle.label | input |
+| test.ps1:199:46:199:51 | input | semmle.label | input |
+| test.ps1:200:46:200:51 | input | semmle.label | input |
+| test.ps1:201:46:201:51 | input | semmle.label | input |
+| test.ps1:202:46:202:51 | input | semmle.label | input |
+| test.ps1:203:46:203:51 | input | semmle.label | input |
+| test.ps1:204:46:204:51 | input | semmle.label | input |
+| test.ps1:206:48:206:53 | input | semmle.label | input |
+| test.ps1:207:48:207:53 | input | semmle.label | input |
+| test.ps1:208:48:208:53 | input | semmle.label | input |
+| test.ps1:210:41:210:46 | input | semmle.label | input |
+| test.ps1:211:41:211:46 | input | semmle.label | input |
+| test.ps1:212:36:212:41 | input | semmle.label | input |
+| test.ps1:213:36:213:41 | input | semmle.label | input |
+| test.ps1:214:36:214:41 | input | semmle.label | input |
+| test.ps1:218:42:218:47 | input | semmle.label | input |
+| test.ps1:219:42:219:47 | input | semmle.label | input |
+| test.ps1:221:33:221:38 | input | semmle.label | input |
+subpaths
+#select
+| test.ps1:4:23:4:52 | Get-Process -Name $UserInput | test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:4:23:4:52 | Get-Process -Name $UserInput | Possible injection path from user input to dangerous call to Invoke-Expression |
+| test.ps1:10:9:10:38 | Get-Process -Name $UserInput | test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:10:9:10:38 | Get-Process -Name $UserInput | Possible injection path from user input to dangerous call to Invoke-Expression |
+| test.ps1:16:50:16:79 | Get-Process -Name $UserInput | test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:16:50:16:79 | Get-Process -Name $UserInput | Possible injection path from user input to dangerous call to InvokeScript |
+| test.ps1:22:41:22:70 | Get-Process -Name $UserInput | test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:22:41:22:70 | Get-Process -Name $UserInput | Possible injection path from user input to dangerous call to CreateNestedPipeline |
+| test.ps1:22:41:22:70 | Get-Process -Name $UserInput | test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:22:41:22:70 | Get-Process -Name $UserInput | Possible injection path from user input to dangerous call to Invoke |
+| test.ps1:22:60:22:69 | UserInput | test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:22:60:22:69 | UserInput | Possible injection path from user input to dangerous call to Invoke |
+| test.ps1:28:38:28:67 | Get-Process -Name $UserInput | test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:28:38:28:67 | Get-Process -Name $UserInput | Possible injection path from user input to dangerous call to AddScript |
+| test.ps1:28:38:28:67 | Get-Process -Name $UserInput | test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:28:38:28:67 | Get-Process -Name $UserInput | Possible injection path from user input to dangerous call to Invoke |
+| test.ps1:28:57:28:66 | UserInput | test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:28:57:28:66 | UserInput | Possible injection path from user input to dangerous call to Invoke |
+| test.ps1:34:14:34:46 | public class Foo { $UserInput } | test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:34:14:34:46 | public class Foo { $UserInput } | Possible injection path from user input to dangerous call to Invoke-Expression |
+| test.ps1:40:30:40:62 | public class Foo { $UserInput } | test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:40:30:40:62 | public class Foo { $UserInput } | Possible injection path from user input to dangerous call to Invoke-Expression |
+| test.ps1:48:30:48:34 | code | test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:48:30:48:34 | code | Possible injection path from user input to dangerous call to Invoke-Expression |
+| test.ps1:75:25:75:54 | Get-Process -Name $UserInput | test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:75:25:75:54 | Get-Process -Name $UserInput | Possible injection path from user input to dangerous call to Powershell |
+| test.ps1:82:16:82:45 | Get-Process -Name $UserInput | test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:82:16:82:45 | Get-Process -Name $UserInput | Possible injection path from user input to dangerous call to Powershell |
+| test.ps1:89:12:89:28 | ping $UserInput | test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:89:12:89:28 | ping $UserInput | Possible injection path from user input to dangerous call to Cmd |
+| test.ps1:106:33:106:62 | Get-Process -Name $UserInput | test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:106:33:106:62 | Get-Process -Name $UserInput | Possible injection path from user input to dangerous call to CreateScriptBlock |
+| test.ps1:116:58:116:87 | Get-Process -Name $UserInput | test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:116:58:116:87 | Get-Process -Name $UserInput | Possible injection path from user input to dangerous call to NewScriptBlock |
+| test.ps1:124:34:124:43 | UserInput | test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:124:34:124:43 | UserInput | Possible injection path from user input to dangerous call to ForEach-Object |
+| test.ps1:131:28:131:37 | UserInput | test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:131:28:131:37 | UserInput | Possible injection path from user input to dangerous call to Invoke |
+| test.ps1:138:28:138:37 | UserInput | test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:138:28:138:37 | UserInput | Possible injection path from user input to dangerous call to Invoke |
+| test.ps1:168:50:168:59 | UserInput | test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:168:50:168:59 | UserInput | Possible injection path from user input to dangerous call to ExpandString |
+| test.ps1:176:63:176:72 | UserInput | test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:176:63:176:72 | UserInput | Possible injection path from user input to dangerous call to ExpandString |
+| test.ps1:192:23:192:54 | Get-Process -Name "$escaped" | test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:192:23:192:54 | Get-Process -Name "$escaped" | Possible injection path from user input to dangerous call to Invoke-Expression |
diff --git a/powershell/ql/test/query-tests/security/cwe-078/InjectionHunter/InjectionHunter.qlref b/powershell/ql/test/query-tests/security/cwe-078/InjectionHunter/InjectionHunter.qlref
new file mode 100644
index 00000000000..61447f65050
--- /dev/null
+++ b/powershell/ql/test/query-tests/security/cwe-078/InjectionHunter/InjectionHunter.qlref
@@ -0,0 +1 @@
+queries/security/cwe-078/InjectionHunter/UserInputToDangerousMethod.ql
\ No newline at end of file
diff --git a/powershell/ql/test/query-tests/security/cwe-078/InjectionHunter/test.ps1 b/powershell/ql/test/query-tests/security/cwe-078/InjectionHunter/test.ps1
new file mode 100644
index 00000000000..3757d9d4f2c
--- /dev/null
+++ b/powershell/ql/test/query-tests/security/cwe-078/InjectionHunter/test.ps1
@@ -0,0 +1,221 @@
+function Invoke-InvokeExpressionInjection1
+{
+ param($UserInput)
+ Invoke-Expression "Get-Process -Name $UserInput"
+}
+
+function Invoke-InvokeExpressionInjection2
+{
+ param($UserInput)
+ iex "Get-Process -Name $UserInput"
+}
+
+function Invoke-InvokeExpressionInjection3
+{
+ param($UserInput)
+ $executionContext.InvokeCommand.InvokeScript("Get-Process -Name $UserInput")
+}
+
+function Invoke-InvokeExpressionInjection4
+{
+ param($UserInput)
+ $host.Runspace.CreateNestedPipeline("Get-Process -Name $UserInput", $false).Invoke()
+}
+
+function Invoke-InvokeExpressionInjection5
+{
+ param($UserInput)
+ [PowerShell]::Create().AddScript("Get-Process -Name $UserInput").Invoke()
+}
+
+function Invoke-InvokeExpressionInjection6
+{
+ param($UserInput)
+ Add-Type "public class Foo { $UserInput }"
+}
+
+function Invoke-InvokeExpressionInjection7
+{
+ param($UserInput)
+ Add-Type -TypeDefinition "public class Foo { $UserInput }"
+}
+
+function Invoke-InvokeExpressionInjection8
+{
+ param($UserInput)
+
+ $code = "public class Foo { $UserInput }"
+ Add-Type -TypeDefinition $code
+}
+
+function Invoke-InvokeExpressionInjectionFP
+{
+ param($UserInput)
+
+ $code = @"
+ public class BasicTest
+ {
+ public static int Add(int a, int b)
+ {
+ return (a + b);
+ }
+ public int Multiply(int a, int b)
+ {
+ return (a * b);
+ }
+ }
+"@
+ Add-Type -TypeDefinition $code
+}
+
+function Invoke-ExploitableCommandInjection1
+{
+ param($UserInput)
+
+ powershell -command "Get-Process -Name $UserInput"
+}
+
+function Invoke-ExploitableCommandInjection2
+{
+ param($UserInput)
+
+ powershell "Get-Process -Name $UserInput"
+}
+
+function Invoke-ExploitableCommandInjection3
+{
+ param($UserInput)
+
+ cmd /c "ping $UserInput"
+}
+
+#Allowed
+function Invoke-ExploitableCommandInjectionFP
+{
+ param($UserInput)
+
+ cmd /c "ping localhost"
+}
+
+function Invoke-ScriptBlockInjection1
+{
+ param($UserInput)
+
+ ## Often used when making remote connections
+
+ $sb = [ScriptBlock]::Create("Get-Process -Name $UserInput")
+ Invoke-Command RemoteServer $sb
+}
+
+function Invoke-ScriptBlockInjection2
+{
+ param($UserInput)
+
+ ## Often used when making remote connections
+
+ $sb = $executionContext.InvokeCommand.NewScriptBlock("Get-Process -Name $UserInput")
+ Invoke-Command RemoteServer $sb
+}
+
+function Invoke-MethodInjection1
+{
+ param($UserInput)
+
+ Get-Process | Foreach-Object $UserInput
+}
+
+function Invoke-MethodInjection2
+{
+ param($UserInput)
+
+ (Get-Process -Id $pid).$UserInput()
+}
+
+function Invoke-MethodInjection3
+{
+ param($UserInput)
+
+ (Get-Process -Id $pid).$UserInput.Invoke()
+}
+
+#ALLOWED , uses script block
+function Invoke-MethodInjectionFP1
+{
+ param($UserInput)
+
+ Get-Process | Foreach-Object { $_.Name }
+}
+#ALLOWED, uses constant member access
+function Invoke-MethodInjectionFP2
+{
+ param($UserInput)
+
+ Get-Process | Foreach-Object "Name"
+}
+
+function Invoke-PropertyInjection
+{
+ param($UserInput)
+
+ [DateTime]::$UserInput
+}
+
+function Invoke-ExpandStringInjection1
+{
+ param($UserInput)
+
+ ## Used to attempt a variable resolution
+ $executionContext.InvokeCommand.ExpandString($UserInput)
+}
+
+function Invoke-ExpandStringInjection2
+{
+ param($UserInput)
+
+ ## Used to attempt a variable resolution
+ $executionContext.SessionState.InvokeCommand.ExpandString($UserInput)
+}
+
+function Invoke-UnsafeEscape1
+{
+ param($UserInput)
+
+ $escaped = $UserInput -replace "'", "''"
+ Invoke-Expression "Get-Process -Name '$escaped'"
+}
+
+function Invoke-UnsafeEscape2
+{
+ param($UserInput)
+
+ $escaped = $UserInput -replace '"', '`"'
+ Invoke-Expression "Get-Process -Name `"$escaped`""
+}
+
+$input = Read-Host "enter input"
+
+Invoke-InvokeExpressionInjection1 -UserInput $input
+Invoke-InvokeExpressionInjection2 -UserInput $input
+Invoke-InvokeExpressionInjection3 -UserInput $input
+Invoke-InvokeExpressionInjection4 -UserInput $input
+Invoke-InvokeExpressionInjection5 -UserInput $input
+Invoke-InvokeExpressionInjection6 -UserInput $input
+Invoke-InvokeExpressionInjection7 -UserInput $input
+Invoke-InvokeExpressionInjection8 -UserInput $input
+Invoke-InvokeExpressionInjectionFP -UserInput $input
+Invoke-ExploitableCommandInjection1 -UserInput $input
+Invoke-ExploitableCommandInjection2 -UserInput $input
+Invoke-ExploitableCommandInjection3 -UserInput $input
+Invoke-ExploitableCommandInjectionFP -UserInput $input
+Invoke-ScriptBlockInjection1 -UserInput $input
+Invoke-ScriptBlockInjection2 -UserInput $input
+Invoke-MethodInjection1 -UserInput $input
+Invoke-MethodInjection2 -UserInput $input
+Invoke-MethodInjection3 -UserInput $input
+Invoke-MethodInjectionFP1 -UserInput $input
+Invoke-MethodInjectionFP2 -UserInput $input
+Invoke-PropertyInjection -UserInput $input
+Invoke-ExpandStringInjection1 -UserInput $input
+Invoke-ExpandStringInjection2 -UserInput $input
+Invoke-UnsafeEscape1 -UserInput $input
+Invoke-UnsafeEscape2 -UserInput $input
\ No newline at end of file
From ed553d393b7bbe6c80974a07fb72c84aa9c287ab Mon Sep 17 00:00:00 2001
From: Chanel Young
Date: Wed, 16 Apr 2025 14:32:30 -0700
Subject: [PATCH 5/6] merged work into CommandInjection query
---
.../CommandInjectionCustomizations.qll | 168 ++++++++++++-
.../security/cwe-078/CommandInjection.qhelp | 20 +-
.../cwe-078/InjectionHunter/Sanitizers.qll | 26 ---
.../cwe-078/InjectionHunter/Sinks.qll | 152 ------------
.../UserInputToDangerousMethod.qhelp | 48 ----
.../UserInputToDangerousMethod.ql | 36 ---
.../cwe-078/CommandInjection/test.ps1 | 211 ++++++++++++++++-
.../InjectionHunter/InjectionHunter.expected | 146 ------------
.../InjectionHunter/InjectionHunter.qlref | 1 -
.../security/cwe-078/InjectionHunter/test.ps1 | 221 ------------------
10 files changed, 392 insertions(+), 637 deletions(-)
delete mode 100644 powershell/ql/src/queries/security/cwe-078/InjectionHunter/Sanitizers.qll
delete mode 100644 powershell/ql/src/queries/security/cwe-078/InjectionHunter/Sinks.qll
delete mode 100644 powershell/ql/src/queries/security/cwe-078/InjectionHunter/UserInputToDangerousMethod.qhelp
delete mode 100644 powershell/ql/src/queries/security/cwe-078/InjectionHunter/UserInputToDangerousMethod.ql
delete mode 100644 powershell/ql/test/query-tests/security/cwe-078/InjectionHunter/InjectionHunter.expected
delete mode 100644 powershell/ql/test/query-tests/security/cwe-078/InjectionHunter/InjectionHunter.qlref
delete mode 100644 powershell/ql/test/query-tests/security/cwe-078/InjectionHunter/test.ps1
diff --git a/powershell/ql/lib/semmle/code/powershell/security/CommandInjectionCustomizations.qll b/powershell/ql/lib/semmle/code/powershell/security/CommandInjectionCustomizations.qll
index 1623941fb82..a75fd0028ca 100644
--- a/powershell/ql/lib/semmle/code/powershell/security/CommandInjectionCustomizations.qll
+++ b/powershell/ql/lib/semmle/code/powershell/security/CommandInjectionCustomizations.qll
@@ -5,6 +5,7 @@
*/
private import semmle.code.powershell.dataflow.DataFlow
+import semmle.code.powershell.ApiGraphs
private import semmle.code.powershell.dataflow.flowsources.FlowSources
private import semmle.code.powershell.Cfg
@@ -20,7 +21,9 @@ module CommandInjection {
/**
* A data flow sink for command-injection vulnerabilities.
*/
- abstract class Sink extends DataFlow::Node { }
+ abstract class Sink extends DataFlow::Node {
+ abstract string getSinkType();
+ }
/**
* A sanitizer for command-injection vulnerabilities.
@@ -39,13 +42,16 @@ module CommandInjection {
SystemCommandExecutionSink() {
// An argument to a call
exists(DataFlow::CallNode call |
- call.getName() = "Invoke-Expression" and
+ call.getName() = ["Invoke-Expression", "iex"] and
call.getAnArgument() = this
)
or
// Or the call command itself in case it's a use of operator &.
any(DataFlow::CallOperatorNode call).getCommand() = this
}
+ override string getSinkType() {
+ result = "call to Invoke-Expression"
+ }
}
class AddTypeSink extends Sink {
@@ -55,11 +61,169 @@ module CommandInjection {
call.getAnArgument() = this
)
}
+ override string getSinkType() {
+ result = "call to Add-Type"
+ }
}
+ class InvokeScriptSink extends Sink {
+ InvokeScriptSink() {
+ exists(API::Node call |
+ API::getTopLevelMember("executioncontext").getMember("invokecommand").getMethod("invokescript") = call and
+ this = call.getArgument(_).asSink()
+ )
+ }
+ override string getSinkType(){
+ result = "call to InvokeScript"
+ }
+}
+
+class CreateNestedPipelineSink extends Sink {
+ CreateNestedPipelineSink() {
+ exists(API::Node call |
+ API::getTopLevelMember("host").getMember("runspace").getMethod("createnestedpipeline") = call and
+ this = call.getArgument(_).asSink()
+ )
+ }
+ override string getSinkType(){
+ result = "call to CreateNestedPipeline"
+ }
+}
+
+class AddScriptInvokeSink extends Sink {
+ AddScriptInvokeSink() {
+ exists(InvokeMemberExpr ie |
+ this.asExpr().getExpr() = ie.getAnArgument() and
+ ie.getName() = "AddScript" and
+ ie.getQualifier().(InvokeMemberExpr).getName() = "Create" and
+ ie.getQualifier().getAChild().toString() = "PowerShell" and
+ ie.getParent().(InvokeMemberExpr).getName() = "Invoke"
+ )
+ }
+ override string getSinkType(){
+ result = "call to AddScript"
+ }
+}
+
+class PowershellSink extends Sink {
+ PowershellSink() {
+ exists( CmdCall c |
+ c.getName() = "powershell" |
+ (
+ this.asExpr().getExpr() = c.getArgument(1) and
+ c.getArgument(0).getValue().toString() = "-command"
+ ) or
+ (
+ this.asExpr().getExpr() = c.getArgument(0)
+ )
+ )
+ }
+ override string getSinkType(){
+ result = "call to Powershell"
+ }
+}
+
+class CmdSink extends Sink {
+ CmdSink() {
+ exists(CmdCall c |
+ this.asExpr().getExpr() = c.getArgument(1) and
+ c.getName() = "cmd" and
+ c.getArgument(0).getValue().toString() = "/c"
+ )
+ }
+ override string getSinkType(){
+ result = "call to Cmd"
+ }
+}
+
+class ForEachObjectSink extends Sink {
+ ForEachObjectSink() {
+ exists(CmdCall c |
+ this.asExpr().getExpr() = c.getAnArgument() and
+ c.getName() = "Foreach-Object"
+ )
+ }
+ override string getSinkType(){
+ result = "call to ForEach-Object"
+ }
+}
+
+class InvokeSink extends Sink {
+ InvokeSink() {
+ exists(InvokeMemberExpr ie |
+ this.asExpr().getExpr() = ie.getCallee() or
+ this.asExpr().getExpr() = ie.getQualifier().getAChild*()
+ )
+ }
+ override string getSinkType(){
+ result = "call to Invoke"
+ }
+}
+
+class CreateScriptBlockSink extends Sink {
+ CreateScriptBlockSink() {
+ exists(InvokeMemberExpr ie |
+ this.asExpr().getExpr() = ie.getAnArgument() and
+ ie.getName() = "Create" and
+ ie.getQualifier().toString() = "ScriptBlock"
+ )
+ }
+ override string getSinkType(){
+ result = "call to CreateScriptBlock"
+ }
+}
+
+class NewScriptBlockSink extends Sink {
+ NewScriptBlockSink() {
+ exists(API::Node call |
+ API::getTopLevelMember("executioncontext").getMember("invokecommand").getMethod("newscriptblock") = call and
+ this = call.getArgument(_).asSink()
+ )
+ }
+ override string getSinkType(){
+ result = "call to NewScriptBlock"
+ }
+}
+
+class ExpandStringSink extends Sink {
+ ExpandStringSink() {
+ exists(API::Node call | this = call.getArgument(_).asSink() |
+ API::getTopLevelMember("executioncontext").getMember("invokecommand").getMethod("expandstring") = call or
+ API::getTopLevelMember("executioncontext").getMember("sessionstate").getMember("invokecommand").getMethod("expandstring") = call
+
+ )
+ }
+ override string getSinkType(){
+ result = "call to ExpandString"
+ }
+}
+
private class ExternalCommandInjectionSink extends Sink {
ExternalCommandInjectionSink() {
this = ModelOutput::getASinkNode("command-injection").asSink()
}
+ override string getSinkType() {
+ result = "external command injection"
+ }
+ }
+
+ class TypedParameterSanitizer extends Sanitizer {
+ TypedParameterSanitizer() {
+ exists(Function f, Parameter p |
+ p = f.getAParameter() and
+ p.getStaticType() != "Object" and
+ this.asParameter() = p
+ )
+ }
+ }
+
+ class SingleQuoteSanitizer extends Sanitizer {
+ SingleQuoteSanitizer() {
+ exists(Expr e, VarReadAccess v |
+ e = this.asExpr().getExpr().getParent() and
+ e.toString().matches("%'$" + v.getVariable().getName() + "'%")
+ )
+ }
}
}
+
diff --git a/powershell/ql/src/queries/security/cwe-078/CommandInjection.qhelp b/powershell/ql/src/queries/security/cwe-078/CommandInjection.qhelp
index b75401a5d70..e89985142d9 100644
--- a/powershell/ql/src/queries/security/cwe-078/CommandInjection.qhelp
+++ b/powershell/ql/src/queries/security/cwe-078/CommandInjection.qhelp
@@ -8,6 +8,21 @@
routine that executes a command, allows the user to execute malicious
code.
+The following are considered dangerous sinks:
+
+ - Invoke-Expression
+ - InvokeScript
+ - CreateNestedPipeline
+ - AddScript
+ - powershell
+ - cmd
+ - Foreach-Object
+ - Invoke
+ - CreateScriptBlock
+ - NewScriptBlock
+ - ExpandString
+
+
@@ -36,7 +51,10 @@ without examining it first.
OWASP:
Command Injection.
-
+
+Injection Hunter:
+PowerShell Injection Hunter: Security Auditing for PowerShell Scripts.
+
diff --git a/powershell/ql/src/queries/security/cwe-078/InjectionHunter/Sanitizers.qll b/powershell/ql/src/queries/security/cwe-078/InjectionHunter/Sanitizers.qll
deleted file mode 100644
index ac635928e10..00000000000
--- a/powershell/ql/src/queries/security/cwe-078/InjectionHunter/Sanitizers.qll
+++ /dev/null
@@ -1,26 +0,0 @@
-import powershell
-import semmle.code.powershell.dataflow.TaintTracking
-import semmle.code.powershell.dataflow.DataFlow
-import semmle.code.powershell.ApiGraphs
-
-
-abstract class Sanitizer extends DataFlow::Node {}
-
-class TypedParameterSanitizer extends Sanitizer {
- TypedParameterSanitizer() {
- exists(Function f, Parameter p |
- p = f.getAParameter() and
- p.getStaticType() != "Object" and
- this.asParameter() = p
- )
- }
-}
-
-class SingleQuoteSanitizer extends Sanitizer {
- SingleQuoteSanitizer() {
- exists(Expr e, VarReadAccess v |
- e = this.asExpr().getExpr().getParent() and
- e.toString().matches("%'$" + v.getVariable().getName() + "'%")
- )
- }
-}
diff --git a/powershell/ql/src/queries/security/cwe-078/InjectionHunter/Sinks.qll b/powershell/ql/src/queries/security/cwe-078/InjectionHunter/Sinks.qll
deleted file mode 100644
index 4c62966746f..00000000000
--- a/powershell/ql/src/queries/security/cwe-078/InjectionHunter/Sinks.qll
+++ /dev/null
@@ -1,152 +0,0 @@
-import powershell
-import semmle.code.powershell.dataflow.TaintTracking
-import semmle.code.powershell.dataflow.DataFlow
-import semmle.code.powershell.ApiGraphs
-import semmle.code.powershell.dataflow.flowsources.FlowSources
-
-abstract class InjectionSink extends DataFlow::Node {
- abstract string getSinkType();
-}
-
-class InvokeExpressionCall extends InjectionSink {
- InvokeExpressionCall() {
- exists(CmdCall c |
- this.asExpr().getExpr() = c.getAnArgument() and
- c.getName() = ["Invoke-Expression", "iex", "Add-Type" ] )
- }
- override string getSinkType(){
- result = "call to Invoke-Expression"
- }
-}
-
-class InvokeScriptSink extends InjectionSink {
- InvokeScriptSink() {
- exists(API::Node call |
- API::getTopLevelMember("executioncontext").getMember("invokecommand").getMethod("invokescript") = call and
- this = call.getArgument(_).asSink()
- )
- }
- override string getSinkType(){
- result = "call to InvokeScript"
- }
-}
-
-class CreateNestedPipelineSink extends InjectionSink {
- CreateNestedPipelineSink() {
- exists(API::Node call |
- API::getTopLevelMember("host").getMember("runspace").getMethod("createnestedpipeline") = call and
- this = call.getArgument(_).asSink()
- )
- }
- override string getSinkType(){
- result = "call to CreateNestedPipeline"
- }
-}
-
-class AddScriptInvokeSink extends InjectionSink {
- AddScriptInvokeSink() {
- exists(InvokeMemberExpr ie |
- this.asExpr().getExpr() = ie.getAnArgument() and
- ie.getName() = "AddScript" and
- ie.getQualifier().(InvokeMemberExpr).getName() = "Create" and
- ie.getQualifier().getAChild().toString() = "PowerShell" and
- ie.getParent().(InvokeMemberExpr).getName() = "Invoke"
- )
- }
- override string getSinkType(){
- result = "call to AddScript"
- }
-}
-
-class PowershellSink extends InjectionSink {
- PowershellSink() {
- exists( CmdCall c |
- c.getName() = "powershell" |
- (
- this.asExpr().getExpr() = c.getArgument(1) and
- c.getArgument(0).getValue().toString() = "-command"
- ) or
- (
- this.asExpr().getExpr() = c.getArgument(0)
- )
- )
- }
- override string getSinkType(){
- result = "call to Powershell"
- }
-}
-
-class CmdSink extends InjectionSink {
- CmdSink() {
- exists(CmdCall c |
- this.asExpr().getExpr() = c.getArgument(1) and
- c.getName() = "cmd" and
- c.getArgument(0).getValue().toString() = "/c"
- )
- }
- override string getSinkType(){
- result = "call to Cmd"
- }
-}
-
-class ForEachObjectSink extends InjectionSink {
- ForEachObjectSink() {
- exists(CmdCall c |
- this.asExpr().getExpr() = c.getAnArgument() and
- c.getName() = "Foreach-Object"
- )
- }
- override string getSinkType(){
- result = "call to ForEach-Object"
- }
-}
-
-class InvokeSink extends InjectionSink {
- InvokeSink() {
- exists(InvokeMemberExpr ie |
- this.asExpr().getExpr() = ie.getCallee() or
- this.asExpr().getExpr() = ie.getQualifier().getAChild*()
- )
- }
- override string getSinkType(){
- result = "call to Invoke"
- }
-}
-
-class CreateScriptBlockSink extends InjectionSink {
- CreateScriptBlockSink() {
- exists(InvokeMemberExpr ie |
- this.asExpr().getExpr() = ie.getAnArgument() and
- ie.getName() = "Create" and
- ie.getQualifier().toString() = "ScriptBlock"
- )
- }
- override string getSinkType(){
- result = "call to CreateScriptBlock"
- }
-}
-
-class NewScriptBlockSink extends InjectionSink {
- NewScriptBlockSink() {
- exists(API::Node call |
- API::getTopLevelMember("executioncontext").getMember("invokecommand").getMethod("newscriptblock") = call and
- this = call.getArgument(_).asSink()
- )
- }
- override string getSinkType(){
- result = "call to NewScriptBlock"
- }
-}
-
-class ExpandStringSink extends InjectionSink {
- ExpandStringSink() {
- exists(API::Node call | this = call.getArgument(_).asSink() |
- API::getTopLevelMember("executioncontext").getMember("invokecommand").getMethod("expandstring") = call or
- API::getTopLevelMember("executioncontext").getMember("sessionstate").getMember("invokecommand").getMethod("expandstring") = call
-
- )
- }
- override string getSinkType(){
- result = "call to ExpandString"
- }
-}
\ No newline at end of file
diff --git a/powershell/ql/src/queries/security/cwe-078/InjectionHunter/UserInputToDangerousMethod.qhelp b/powershell/ql/src/queries/security/cwe-078/InjectionHunter/UserInputToDangerousMethod.qhelp
deleted file mode 100644
index de459c2e84f..00000000000
--- a/powershell/ql/src/queries/security/cwe-078/InjectionHunter/UserInputToDangerousMethod.qhelp
+++ /dev/null
@@ -1,48 +0,0 @@
-
-
-
-Code that passes user input directly to
-Invoke-Expression, &, or some other library
-routine that executes a command, allows the user to execute malicious
-code.
-
-This is a port of the InjectionHunter tool by Lee Holmes, and checks when user input is passed to any of the following:
-
- - Invoke-Expression
- - InvokeScript
- - CreateNestedPipeline
- - AddScript
- - powershell
- - cmd
- - Foreach-Object
- - Invoke
- - CreateScriptBlock
- - NewScriptBlock
- - ExpandString
-
-
-
-
-
-Possible script injection risk. Untrusted input can cause arbitrary PowerShell expressions to be run.
-Variables may be used directly for dynamic parameter arguments, splatting can be used for dynamic parameter names,
-and the invocation operator can be used for dynamic command names. If content escaping is truly needed, PowerShell has several valid quote characters,
-so [System.Management.Automation.Language.CodeGeneration]::Escape* should be used.
-
-
-
-
-
-
-OWASP:
-Command Injection.
-
-
-Injection Hunter:
-PowerShell Injection Hunter: Security Auditing for PowerShell Scripts.
-
-
-
-
diff --git a/powershell/ql/src/queries/security/cwe-078/InjectionHunter/UserInputToDangerousMethod.ql b/powershell/ql/src/queries/security/cwe-078/InjectionHunter/UserInputToDangerousMethod.ql
deleted file mode 100644
index 11730a65299..00000000000
--- a/powershell/ql/src/queries/security/cwe-078/InjectionHunter/UserInputToDangerousMethod.ql
+++ /dev/null
@@ -1,36 +0,0 @@
-/**
- * @name User Input to injection sink
- * @description Finding cases where the user input is passed an dangerous method that can lead to RCE
- * @kind path-problem
- * @problem.severity error
- * @security-severity 9.8
- * @precision high
- * @id powershell/microsoft/public/user-input-to-injection-sink
- * @tags security
- * external/cwe/cwe-078
- * external/cwe/cwe-088
- */
-
-import powershell
-import semmle.code.powershell.dataflow.TaintTracking
-import semmle.code.powershell.dataflow.DataFlow
-import semmle.code.powershell.ApiGraphs
-import semmle.code.powershell.dataflow.flowsources.FlowSources
-
-import Sanitizers
-import Sinks
-
-private module InjectionConfig implements DataFlow::ConfigSig {
- predicate isSource(DataFlow::Node source) {
- source instanceof SourceNode
- }
- predicate isSink(DataFlow::Node sink) { sink instanceof InjectionSink }
- predicate isBarrier(DataFlow::Node node) {node instanceof Sanitizer}
-}
-
-module InjectionFlow = TaintTracking::Global;
-import InjectionFlow::PathGraph
-
-from InjectionFlow::PathNode source, InjectionFlow::PathNode sink
-where InjectionFlow::flowPath(source, sink)
-select sink.getNode(), source, sink, "Possible injection path from user input to dangerous " + sink.getNode().(InjectionSink).getSinkType()
diff --git a/powershell/ql/test/query-tests/security/cwe-078/CommandInjection/test.ps1 b/powershell/ql/test/query-tests/security/cwe-078/CommandInjection/test.ps1
index 682b1af3752..fd1bc38ce08 100644
--- a/powershell/ql/test/query-tests/security/cwe-078/CommandInjection/test.ps1
+++ b/powershell/ql/test/query-tests/security/cwe-078/CommandInjection/test.ps1
@@ -1,7 +1,210 @@
-param ($x)
+function Invoke-InvokeExpressionInjection1
+{
+ param($UserInput)
+ Invoke-Expression "Get-Process -Name $UserInput"
+}
-Invoke-Expression -Command "Get-Process -Id $x" # BAD
+function Invoke-InvokeExpressionInjection2
+{
+ param($UserInput)
+ iex "Get-Process -Name $UserInput"
+}
-$code = "$Env:MY_VAR"
+function Invoke-InvokeExpressionInjection3
+{
+ param($UserInput)
+ $executionContext.InvokeCommand.InvokeScript("Get-Process -Name $UserInput")
+}
-& "$code --enabled" # BAD
\ No newline at end of file
+function Invoke-InvokeExpressionInjection4
+{
+ param($UserInput)
+ $host.Runspace.CreateNestedPipeline("Get-Process -Name $UserInput", $false).Invoke()
+}
+
+function Invoke-InvokeExpressionInjection5
+{
+ param($UserInput)
+ [PowerShell]::Create().AddScript("Get-Process -Name $UserInput").Invoke()
+}
+
+function Invoke-InvokeExpressionInjection6
+{
+ param($UserInput)
+ Add-Type "public class Foo { $UserInput }"
+}
+
+function Invoke-InvokeExpressionInjection7
+{
+ param($UserInput)
+ Add-Type -TypeDefinition "public class Foo { $UserInput }"
+}
+
+function Invoke-InvokeExpressionInjection8
+{
+ param($UserInput)
+
+ $code = "public class Foo { $UserInput }"
+ Add-Type -TypeDefinition $code
+}
+
+function Invoke-InvokeExpressionInjectionFP
+{
+ param($UserInput)
+
+ $code = @"
+ public class BasicTest
+ {
+ public static int Add(int a, int b)
+ {
+ return (a + b);
+ }
+ public int Multiply(int a, int b)
+ {
+ return (a * b);
+ }
+ }
+"@
+ Add-Type -TypeDefinition $code
+}
+
+function Invoke-ExploitableCommandInjection1
+{
+ param($UserInput)
+
+ powershell -command "Get-Process -Name $UserInput"
+}
+
+function Invoke-ExploitableCommandInjection2
+{
+ param($UserInput)
+
+ powershell "Get-Process -Name $UserInput"
+}
+
+function Invoke-ExploitableCommandInjection3
+{
+ param($UserInput)
+
+ cmd /c "ping $UserInput"
+}
+
+function Invoke-ScriptBlockInjection1
+{
+ param($UserInput)
+
+ ## Often used when making remote connections
+
+ $sb = [ScriptBlock]::Create("Get-Process -Name $UserInput")
+ Invoke-Command RemoteServer $sb
+}
+
+function Invoke-ScriptBlockInjection2
+{
+ param($UserInput)
+
+ ## Often used when making remote connections
+
+ $sb = $executionContext.InvokeCommand.NewScriptBlock("Get-Process -Name $UserInput")
+ Invoke-Command RemoteServer $sb
+}
+
+function Invoke-MethodInjection1
+{
+ param($UserInput)
+
+ Get-Process | Foreach-Object $UserInput
+}
+
+function Invoke-MethodInjection2
+{
+ param($UserInput)
+
+ (Get-Process -Id $pid).$UserInput()
+}
+
+function Invoke-MethodInjection3
+{
+ param($UserInput)
+
+ (Get-Process -Id $pid).$UserInput.Invoke()
+}
+
+#TODO: currently a FN
+function Invoke-ExpandStringInjection1
+{
+ param($UserInput)
+
+ ## Used to attempt a variable resolution
+ $executionContext.InvokeCommand.ExpandString($UserInput)
+}
+
+function Invoke-ExpandStringInjection2
+{
+ param($UserInput)
+
+ ## Used to attempt a variable resolution
+ $executionContext.SessionState.InvokeCommand.ExpandString($UserInput)
+}
+
+
+
+$input = Read-Host "enter input"
+
+Invoke-InvokeExpressionInjection1 -UserInput $input
+Invoke-InvokeExpressionInjection2 -UserInput $input
+Invoke-InvokeExpressionInjection3 -UserInput $input
+Invoke-InvokeExpressionInjection4 -UserInput $input
+Invoke-InvokeExpressionInjection5 -UserInput $input
+Invoke-InvokeExpressionInjection6 -UserInput $input
+Invoke-InvokeExpressionInjection7 -UserInput $input
+Invoke-InvokeExpressionInjection8 -UserInput $input
+Invoke-InvokeExpressionInjectionFP -UserInput $input
+Invoke-ExploitableCommandInjection1 -UserInput $input
+Invoke-ExploitableCommandInjection2 -UserInput $input
+Invoke-ExploitableCommandInjection3 -UserInput $input
+Invoke-ScriptBlockInjection1 -UserInput $input
+Invoke-ScriptBlockInjection2 -UserInput $input
+Invoke-MethodInjection1 -UserInput $input
+Invoke-MethodInjection2 -UserInput $input
+Invoke-MethodInjection3 -UserInput $input
+Invoke-PropertyInjection -UserInput $input
+Invoke-ExpandStringInjection1 -UserInput $input
+Invoke-ExpandStringInjection2 -UserInput $input
+
+#typed input
+function Invoke-InvokeExpressionInjectionSafe1
+{
+ param([int] $UserInput)
+ Invoke-Expression "Get-Process -Name $UserInput"
+}
+
+#single quotes to treat them as string literal
+function Invoke-InvokeExpressionInjectionSafe2
+{
+ param($UserInput)
+ Invoke-Expression "Get-Process -Name '$UserInput'"
+}
+#EscapeSingleQuotedStringContent API
+function Invoke-InvokeExpressionInjectionSafe3
+{
+ param([int] $UserInput)
+
+ $UserInputClean = [System.Management.Automation.Language.CodeGeneration]::
+ EscapeSingleQuotedStringContent("$UserInput")
+ Invoke-Expression "Get-Process -Name $UserInputClean"
+}
+
+#EscapeSingleQuotedStringContent API 2
+function Invoke-InvokeExpressionInjectionSafe4
+{
+ param([int] $UserInput)
+
+ $UserInputClean = [System.Management.Automation.Language.CodeGeneration]::EscapeSingleQuotedStringContent("$UserInput")
+ Invoke-Expression "Get-Process -Name $UserInputClean"
+}
+
+Invoke-InvokeExpressionInjectionSafe1 -UserInput $input
+Invoke-InvokeExpressionInjectionSafe2 -UserInput $input
+Invoke-InvokeExpressionInjectionSafe3 -UserInput $input
+Invoke-InvokeExpressionInjectionSafe4 -UserInput $input
\ No newline at end of file
diff --git a/powershell/ql/test/query-tests/security/cwe-078/InjectionHunter/InjectionHunter.expected b/powershell/ql/test/query-tests/security/cwe-078/InjectionHunter/InjectionHunter.expected
deleted file mode 100644
index 8bfcc1dafec..00000000000
--- a/powershell/ql/test/query-tests/security/cwe-078/InjectionHunter/InjectionHunter.expected
+++ /dev/null
@@ -1,146 +0,0 @@
-edges
-| test.ps1:3:11:3:20 | UserInput | test.ps1:4:23:4:52 | Get-Process -Name $UserInput | provenance | |
-| test.ps1:9:11:9:20 | UserInput | test.ps1:10:9:10:38 | Get-Process -Name $UserInput | provenance | |
-| test.ps1:15:11:15:20 | UserInput | test.ps1:16:50:16:79 | Get-Process -Name $UserInput | provenance | |
-| test.ps1:21:11:21:20 | UserInput | test.ps1:22:41:22:70 | Get-Process -Name $UserInput | provenance | |
-| test.ps1:21:11:21:20 | UserInput | test.ps1:22:60:22:69 | UserInput | provenance | |
-| test.ps1:27:11:27:20 | UserInput | test.ps1:28:38:28:67 | Get-Process -Name $UserInput | provenance | |
-| test.ps1:27:11:27:20 | UserInput | test.ps1:28:57:28:66 | UserInput | provenance | |
-| test.ps1:33:11:33:20 | UserInput | test.ps1:34:14:34:46 | public class Foo { $UserInput } | provenance | |
-| test.ps1:39:11:39:20 | UserInput | test.ps1:40:30:40:62 | public class Foo { $UserInput } | provenance | |
-| test.ps1:45:11:45:20 | UserInput | test.ps1:48:30:48:34 | code | provenance | |
-| test.ps1:73:11:73:20 | UserInput | test.ps1:75:25:75:54 | Get-Process -Name $UserInput | provenance | |
-| test.ps1:80:11:80:20 | UserInput | test.ps1:82:16:82:45 | Get-Process -Name $UserInput | provenance | |
-| test.ps1:87:11:87:20 | UserInput | test.ps1:89:12:89:28 | ping $UserInput | provenance | |
-| test.ps1:102:11:102:20 | UserInput | test.ps1:106:33:106:62 | Get-Process -Name $UserInput | provenance | |
-| test.ps1:112:11:112:20 | UserInput | test.ps1:116:58:116:87 | Get-Process -Name $UserInput | provenance | |
-| test.ps1:122:11:122:20 | UserInput | test.ps1:124:34:124:43 | UserInput | provenance | |
-| test.ps1:129:11:129:20 | UserInput | test.ps1:131:28:131:37 | UserInput | provenance | |
-| test.ps1:136:11:136:20 | UserInput | test.ps1:138:28:138:37 | UserInput | provenance | |
-| test.ps1:165:11:165:20 | UserInput | test.ps1:168:50:168:59 | UserInput | provenance | |
-| test.ps1:173:11:173:20 | UserInput | test.ps1:176:63:176:72 | UserInput | provenance | |
-| test.ps1:189:11:189:20 | UserInput | test.ps1:192:23:192:54 | Get-Process -Name "$escaped" | provenance | |
-| test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:197:46:197:51 | input | provenance | Src:MaD:11464 |
-| test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:198:46:198:51 | input | provenance | Src:MaD:11464 |
-| test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:199:46:199:51 | input | provenance | Src:MaD:11464 |
-| test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:200:46:200:51 | input | provenance | Src:MaD:11464 |
-| test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:201:46:201:51 | input | provenance | Src:MaD:11464 |
-| test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:202:46:202:51 | input | provenance | Src:MaD:11464 |
-| test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:203:46:203:51 | input | provenance | Src:MaD:11464 |
-| test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:204:46:204:51 | input | provenance | Src:MaD:11464 |
-| test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:206:48:206:53 | input | provenance | Src:MaD:11464 |
-| test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:207:48:207:53 | input | provenance | Src:MaD:11464 |
-| test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:208:48:208:53 | input | provenance | Src:MaD:11464 |
-| test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:210:41:210:46 | input | provenance | Src:MaD:11464 |
-| test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:211:41:211:46 | input | provenance | Src:MaD:11464 |
-| test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:212:36:212:41 | input | provenance | Src:MaD:11464 |
-| test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:213:36:213:41 | input | provenance | Src:MaD:11464 |
-| test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:214:36:214:41 | input | provenance | Src:MaD:11464 |
-| test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:218:42:218:47 | input | provenance | Src:MaD:11464 |
-| test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:219:42:219:47 | input | provenance | Src:MaD:11464 |
-| test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:221:33:221:38 | input | provenance | Src:MaD:11464 |
-| test.ps1:197:46:197:51 | input | test.ps1:3:11:3:20 | UserInput | provenance | |
-| test.ps1:198:46:198:51 | input | test.ps1:9:11:9:20 | UserInput | provenance | |
-| test.ps1:199:46:199:51 | input | test.ps1:15:11:15:20 | UserInput | provenance | |
-| test.ps1:200:46:200:51 | input | test.ps1:21:11:21:20 | UserInput | provenance | |
-| test.ps1:201:46:201:51 | input | test.ps1:27:11:27:20 | UserInput | provenance | |
-| test.ps1:202:46:202:51 | input | test.ps1:33:11:33:20 | UserInput | provenance | |
-| test.ps1:203:46:203:51 | input | test.ps1:39:11:39:20 | UserInput | provenance | |
-| test.ps1:204:46:204:51 | input | test.ps1:45:11:45:20 | UserInput | provenance | |
-| test.ps1:206:48:206:53 | input | test.ps1:73:11:73:20 | UserInput | provenance | |
-| test.ps1:207:48:207:53 | input | test.ps1:80:11:80:20 | UserInput | provenance | |
-| test.ps1:208:48:208:53 | input | test.ps1:87:11:87:20 | UserInput | provenance | |
-| test.ps1:210:41:210:46 | input | test.ps1:102:11:102:20 | UserInput | provenance | |
-| test.ps1:211:41:211:46 | input | test.ps1:112:11:112:20 | UserInput | provenance | |
-| test.ps1:212:36:212:41 | input | test.ps1:122:11:122:20 | UserInput | provenance | |
-| test.ps1:213:36:213:41 | input | test.ps1:129:11:129:20 | UserInput | provenance | |
-| test.ps1:214:36:214:41 | input | test.ps1:136:11:136:20 | UserInput | provenance | |
-| test.ps1:218:42:218:47 | input | test.ps1:165:11:165:20 | UserInput | provenance | |
-| test.ps1:219:42:219:47 | input | test.ps1:173:11:173:20 | UserInput | provenance | |
-| test.ps1:221:33:221:38 | input | test.ps1:189:11:189:20 | UserInput | provenance | |
-nodes
-| test.ps1:3:11:3:20 | UserInput | semmle.label | UserInput |
-| test.ps1:4:23:4:52 | Get-Process -Name $UserInput | semmle.label | Get-Process -Name $UserInput |
-| test.ps1:9:11:9:20 | UserInput | semmle.label | UserInput |
-| test.ps1:10:9:10:38 | Get-Process -Name $UserInput | semmle.label | Get-Process -Name $UserInput |
-| test.ps1:15:11:15:20 | UserInput | semmle.label | UserInput |
-| test.ps1:16:50:16:79 | Get-Process -Name $UserInput | semmle.label | Get-Process -Name $UserInput |
-| test.ps1:21:11:21:20 | UserInput | semmle.label | UserInput |
-| test.ps1:22:41:22:70 | Get-Process -Name $UserInput | semmle.label | Get-Process -Name $UserInput |
-| test.ps1:22:60:22:69 | UserInput | semmle.label | UserInput |
-| test.ps1:27:11:27:20 | UserInput | semmle.label | UserInput |
-| test.ps1:28:38:28:67 | Get-Process -Name $UserInput | semmle.label | Get-Process -Name $UserInput |
-| test.ps1:28:57:28:66 | UserInput | semmle.label | UserInput |
-| test.ps1:33:11:33:20 | UserInput | semmle.label | UserInput |
-| test.ps1:34:14:34:46 | public class Foo { $UserInput } | semmle.label | public class Foo { $UserInput } |
-| test.ps1:39:11:39:20 | UserInput | semmle.label | UserInput |
-| test.ps1:40:30:40:62 | public class Foo { $UserInput } | semmle.label | public class Foo { $UserInput } |
-| test.ps1:45:11:45:20 | UserInput | semmle.label | UserInput |
-| test.ps1:48:30:48:34 | code | semmle.label | code |
-| test.ps1:73:11:73:20 | UserInput | semmle.label | UserInput |
-| test.ps1:75:25:75:54 | Get-Process -Name $UserInput | semmle.label | Get-Process -Name $UserInput |
-| test.ps1:80:11:80:20 | UserInput | semmle.label | UserInput |
-| test.ps1:82:16:82:45 | Get-Process -Name $UserInput | semmle.label | Get-Process -Name $UserInput |
-| test.ps1:87:11:87:20 | UserInput | semmle.label | UserInput |
-| test.ps1:89:12:89:28 | ping $UserInput | semmle.label | ping $UserInput |
-| test.ps1:102:11:102:20 | UserInput | semmle.label | UserInput |
-| test.ps1:106:33:106:62 | Get-Process -Name $UserInput | semmle.label | Get-Process -Name $UserInput |
-| test.ps1:112:11:112:20 | UserInput | semmle.label | UserInput |
-| test.ps1:116:58:116:87 | Get-Process -Name $UserInput | semmle.label | Get-Process -Name $UserInput |
-| test.ps1:122:11:122:20 | UserInput | semmle.label | UserInput |
-| test.ps1:124:34:124:43 | UserInput | semmle.label | UserInput |
-| test.ps1:129:11:129:20 | UserInput | semmle.label | UserInput |
-| test.ps1:131:28:131:37 | UserInput | semmle.label | UserInput |
-| test.ps1:136:11:136:20 | UserInput | semmle.label | UserInput |
-| test.ps1:138:28:138:37 | UserInput | semmle.label | UserInput |
-| test.ps1:165:11:165:20 | UserInput | semmle.label | UserInput |
-| test.ps1:168:50:168:59 | UserInput | semmle.label | UserInput |
-| test.ps1:173:11:173:20 | UserInput | semmle.label | UserInput |
-| test.ps1:176:63:176:72 | UserInput | semmle.label | UserInput |
-| test.ps1:189:11:189:20 | UserInput | semmle.label | UserInput |
-| test.ps1:192:23:192:54 | Get-Process -Name "$escaped" | semmle.label | Get-Process -Name "$escaped" |
-| test.ps1:195:10:195:32 | Call to Read-Host | semmle.label | Call to Read-Host |
-| test.ps1:197:46:197:51 | input | semmle.label | input |
-| test.ps1:198:46:198:51 | input | semmle.label | input |
-| test.ps1:199:46:199:51 | input | semmle.label | input |
-| test.ps1:200:46:200:51 | input | semmle.label | input |
-| test.ps1:201:46:201:51 | input | semmle.label | input |
-| test.ps1:202:46:202:51 | input | semmle.label | input |
-| test.ps1:203:46:203:51 | input | semmle.label | input |
-| test.ps1:204:46:204:51 | input | semmle.label | input |
-| test.ps1:206:48:206:53 | input | semmle.label | input |
-| test.ps1:207:48:207:53 | input | semmle.label | input |
-| test.ps1:208:48:208:53 | input | semmle.label | input |
-| test.ps1:210:41:210:46 | input | semmle.label | input |
-| test.ps1:211:41:211:46 | input | semmle.label | input |
-| test.ps1:212:36:212:41 | input | semmle.label | input |
-| test.ps1:213:36:213:41 | input | semmle.label | input |
-| test.ps1:214:36:214:41 | input | semmle.label | input |
-| test.ps1:218:42:218:47 | input | semmle.label | input |
-| test.ps1:219:42:219:47 | input | semmle.label | input |
-| test.ps1:221:33:221:38 | input | semmle.label | input |
-subpaths
-#select
-| test.ps1:4:23:4:52 | Get-Process -Name $UserInput | test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:4:23:4:52 | Get-Process -Name $UserInput | Possible injection path from user input to dangerous call to Invoke-Expression |
-| test.ps1:10:9:10:38 | Get-Process -Name $UserInput | test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:10:9:10:38 | Get-Process -Name $UserInput | Possible injection path from user input to dangerous call to Invoke-Expression |
-| test.ps1:16:50:16:79 | Get-Process -Name $UserInput | test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:16:50:16:79 | Get-Process -Name $UserInput | Possible injection path from user input to dangerous call to InvokeScript |
-| test.ps1:22:41:22:70 | Get-Process -Name $UserInput | test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:22:41:22:70 | Get-Process -Name $UserInput | Possible injection path from user input to dangerous call to CreateNestedPipeline |
-| test.ps1:22:41:22:70 | Get-Process -Name $UserInput | test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:22:41:22:70 | Get-Process -Name $UserInput | Possible injection path from user input to dangerous call to Invoke |
-| test.ps1:22:60:22:69 | UserInput | test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:22:60:22:69 | UserInput | Possible injection path from user input to dangerous call to Invoke |
-| test.ps1:28:38:28:67 | Get-Process -Name $UserInput | test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:28:38:28:67 | Get-Process -Name $UserInput | Possible injection path from user input to dangerous call to AddScript |
-| test.ps1:28:38:28:67 | Get-Process -Name $UserInput | test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:28:38:28:67 | Get-Process -Name $UserInput | Possible injection path from user input to dangerous call to Invoke |
-| test.ps1:28:57:28:66 | UserInput | test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:28:57:28:66 | UserInput | Possible injection path from user input to dangerous call to Invoke |
-| test.ps1:34:14:34:46 | public class Foo { $UserInput } | test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:34:14:34:46 | public class Foo { $UserInput } | Possible injection path from user input to dangerous call to Invoke-Expression |
-| test.ps1:40:30:40:62 | public class Foo { $UserInput } | test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:40:30:40:62 | public class Foo { $UserInput } | Possible injection path from user input to dangerous call to Invoke-Expression |
-| test.ps1:48:30:48:34 | code | test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:48:30:48:34 | code | Possible injection path from user input to dangerous call to Invoke-Expression |
-| test.ps1:75:25:75:54 | Get-Process -Name $UserInput | test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:75:25:75:54 | Get-Process -Name $UserInput | Possible injection path from user input to dangerous call to Powershell |
-| test.ps1:82:16:82:45 | Get-Process -Name $UserInput | test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:82:16:82:45 | Get-Process -Name $UserInput | Possible injection path from user input to dangerous call to Powershell |
-| test.ps1:89:12:89:28 | ping $UserInput | test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:89:12:89:28 | ping $UserInput | Possible injection path from user input to dangerous call to Cmd |
-| test.ps1:106:33:106:62 | Get-Process -Name $UserInput | test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:106:33:106:62 | Get-Process -Name $UserInput | Possible injection path from user input to dangerous call to CreateScriptBlock |
-| test.ps1:116:58:116:87 | Get-Process -Name $UserInput | test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:116:58:116:87 | Get-Process -Name $UserInput | Possible injection path from user input to dangerous call to NewScriptBlock |
-| test.ps1:124:34:124:43 | UserInput | test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:124:34:124:43 | UserInput | Possible injection path from user input to dangerous call to ForEach-Object |
-| test.ps1:131:28:131:37 | UserInput | test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:131:28:131:37 | UserInput | Possible injection path from user input to dangerous call to Invoke |
-| test.ps1:138:28:138:37 | UserInput | test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:138:28:138:37 | UserInput | Possible injection path from user input to dangerous call to Invoke |
-| test.ps1:168:50:168:59 | UserInput | test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:168:50:168:59 | UserInput | Possible injection path from user input to dangerous call to ExpandString |
-| test.ps1:176:63:176:72 | UserInput | test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:176:63:176:72 | UserInput | Possible injection path from user input to dangerous call to ExpandString |
-| test.ps1:192:23:192:54 | Get-Process -Name "$escaped" | test.ps1:195:10:195:32 | Call to Read-Host | test.ps1:192:23:192:54 | Get-Process -Name "$escaped" | Possible injection path from user input to dangerous call to Invoke-Expression |
diff --git a/powershell/ql/test/query-tests/security/cwe-078/InjectionHunter/InjectionHunter.qlref b/powershell/ql/test/query-tests/security/cwe-078/InjectionHunter/InjectionHunter.qlref
deleted file mode 100644
index 61447f65050..00000000000
--- a/powershell/ql/test/query-tests/security/cwe-078/InjectionHunter/InjectionHunter.qlref
+++ /dev/null
@@ -1 +0,0 @@
-queries/security/cwe-078/InjectionHunter/UserInputToDangerousMethod.ql
\ No newline at end of file
diff --git a/powershell/ql/test/query-tests/security/cwe-078/InjectionHunter/test.ps1 b/powershell/ql/test/query-tests/security/cwe-078/InjectionHunter/test.ps1
deleted file mode 100644
index 3757d9d4f2c..00000000000
--- a/powershell/ql/test/query-tests/security/cwe-078/InjectionHunter/test.ps1
+++ /dev/null
@@ -1,221 +0,0 @@
-function Invoke-InvokeExpressionInjection1
-{
- param($UserInput)
- Invoke-Expression "Get-Process -Name $UserInput"
-}
-
-function Invoke-InvokeExpressionInjection2
-{
- param($UserInput)
- iex "Get-Process -Name $UserInput"
-}
-
-function Invoke-InvokeExpressionInjection3
-{
- param($UserInput)
- $executionContext.InvokeCommand.InvokeScript("Get-Process -Name $UserInput")
-}
-
-function Invoke-InvokeExpressionInjection4
-{
- param($UserInput)
- $host.Runspace.CreateNestedPipeline("Get-Process -Name $UserInput", $false).Invoke()
-}
-
-function Invoke-InvokeExpressionInjection5
-{
- param($UserInput)
- [PowerShell]::Create().AddScript("Get-Process -Name $UserInput").Invoke()
-}
-
-function Invoke-InvokeExpressionInjection6
-{
- param($UserInput)
- Add-Type "public class Foo { $UserInput }"
-}
-
-function Invoke-InvokeExpressionInjection7
-{
- param($UserInput)
- Add-Type -TypeDefinition "public class Foo { $UserInput }"
-}
-
-function Invoke-InvokeExpressionInjection8
-{
- param($UserInput)
-
- $code = "public class Foo { $UserInput }"
- Add-Type -TypeDefinition $code
-}
-
-function Invoke-InvokeExpressionInjectionFP
-{
- param($UserInput)
-
- $code = @"
- public class BasicTest
- {
- public static int Add(int a, int b)
- {
- return (a + b);
- }
- public int Multiply(int a, int b)
- {
- return (a * b);
- }
- }
-"@
- Add-Type -TypeDefinition $code
-}
-
-function Invoke-ExploitableCommandInjection1
-{
- param($UserInput)
-
- powershell -command "Get-Process -Name $UserInput"
-}
-
-function Invoke-ExploitableCommandInjection2
-{
- param($UserInput)
-
- powershell "Get-Process -Name $UserInput"
-}
-
-function Invoke-ExploitableCommandInjection3
-{
- param($UserInput)
-
- cmd /c "ping $UserInput"
-}
-
-#Allowed
-function Invoke-ExploitableCommandInjectionFP
-{
- param($UserInput)
-
- cmd /c "ping localhost"
-}
-
-function Invoke-ScriptBlockInjection1
-{
- param($UserInput)
-
- ## Often used when making remote connections
-
- $sb = [ScriptBlock]::Create("Get-Process -Name $UserInput")
- Invoke-Command RemoteServer $sb
-}
-
-function Invoke-ScriptBlockInjection2
-{
- param($UserInput)
-
- ## Often used when making remote connections
-
- $sb = $executionContext.InvokeCommand.NewScriptBlock("Get-Process -Name $UserInput")
- Invoke-Command RemoteServer $sb
-}
-
-function Invoke-MethodInjection1
-{
- param($UserInput)
-
- Get-Process | Foreach-Object $UserInput
-}
-
-function Invoke-MethodInjection2
-{
- param($UserInput)
-
- (Get-Process -Id $pid).$UserInput()
-}
-
-function Invoke-MethodInjection3
-{
- param($UserInput)
-
- (Get-Process -Id $pid).$UserInput.Invoke()
-}
-
-#ALLOWED , uses script block
-function Invoke-MethodInjectionFP1
-{
- param($UserInput)
-
- Get-Process | Foreach-Object { $_.Name }
-}
-#ALLOWED, uses constant member access
-function Invoke-MethodInjectionFP2
-{
- param($UserInput)
-
- Get-Process | Foreach-Object "Name"
-}
-
-function Invoke-PropertyInjection
-{
- param($UserInput)
-
- [DateTime]::$UserInput
-}
-
-function Invoke-ExpandStringInjection1
-{
- param($UserInput)
-
- ## Used to attempt a variable resolution
- $executionContext.InvokeCommand.ExpandString($UserInput)
-}
-
-function Invoke-ExpandStringInjection2
-{
- param($UserInput)
-
- ## Used to attempt a variable resolution
- $executionContext.SessionState.InvokeCommand.ExpandString($UserInput)
-}
-
-function Invoke-UnsafeEscape1
-{
- param($UserInput)
-
- $escaped = $UserInput -replace "'", "''"
- Invoke-Expression "Get-Process -Name '$escaped'"
-}
-
-function Invoke-UnsafeEscape2
-{
- param($UserInput)
-
- $escaped = $UserInput -replace '"', '`"'
- Invoke-Expression "Get-Process -Name `"$escaped`""
-}
-
-$input = Read-Host "enter input"
-
-Invoke-InvokeExpressionInjection1 -UserInput $input
-Invoke-InvokeExpressionInjection2 -UserInput $input
-Invoke-InvokeExpressionInjection3 -UserInput $input
-Invoke-InvokeExpressionInjection4 -UserInput $input
-Invoke-InvokeExpressionInjection5 -UserInput $input
-Invoke-InvokeExpressionInjection6 -UserInput $input
-Invoke-InvokeExpressionInjection7 -UserInput $input
-Invoke-InvokeExpressionInjection8 -UserInput $input
-Invoke-InvokeExpressionInjectionFP -UserInput $input
-Invoke-ExploitableCommandInjection1 -UserInput $input
-Invoke-ExploitableCommandInjection2 -UserInput $input
-Invoke-ExploitableCommandInjection3 -UserInput $input
-Invoke-ExploitableCommandInjectionFP -UserInput $input
-Invoke-ScriptBlockInjection1 -UserInput $input
-Invoke-ScriptBlockInjection2 -UserInput $input
-Invoke-MethodInjection1 -UserInput $input
-Invoke-MethodInjection2 -UserInput $input
-Invoke-MethodInjection3 -UserInput $input
-Invoke-MethodInjectionFP1 -UserInput $input
-Invoke-MethodInjectionFP2 -UserInput $input
-Invoke-PropertyInjection -UserInput $input
-Invoke-ExpandStringInjection1 -UserInput $input
-Invoke-ExpandStringInjection2 -UserInput $input
-Invoke-UnsafeEscape1 -UserInput $input
-Invoke-UnsafeEscape2 -UserInput $input
\ No newline at end of file
From 12b918e900d99724390be6b2e7d974659b5d8a00 Mon Sep 17 00:00:00 2001
From: Chanel Young
Date: Thu, 17 Apr 2025 10:39:42 -0700
Subject: [PATCH 6/6] pr feedback: removed toString, updated .expected
---
.../CommandInjectionCustomizations.qll | 38 ++---
.../CommandInjection.expected | 141 +++++++++++++++++-
2 files changed, 153 insertions(+), 26 deletions(-)
diff --git a/powershell/ql/lib/semmle/code/powershell/security/CommandInjectionCustomizations.qll b/powershell/ql/lib/semmle/code/powershell/security/CommandInjectionCustomizations.qll
index a75fd0028ca..7f2ab885764 100644
--- a/powershell/ql/lib/semmle/code/powershell/security/CommandInjectionCustomizations.qll
+++ b/powershell/ql/lib/semmle/code/powershell/security/CommandInjectionCustomizations.qll
@@ -91,18 +91,19 @@ class CreateNestedPipelineSink extends Sink {
}
class AddScriptInvokeSink extends Sink {
- AddScriptInvokeSink() {
- exists(InvokeMemberExpr ie |
- this.asExpr().getExpr() = ie.getAnArgument() and
- ie.getName() = "AddScript" and
- ie.getQualifier().(InvokeMemberExpr).getName() = "Create" and
- ie.getQualifier().getAChild().toString() = "PowerShell" and
- ie.getParent().(InvokeMemberExpr).getName() = "Invoke"
- )
- }
- override string getSinkType(){
- result = "call to AddScript"
- }
+ AddScriptInvokeSink() {
+ exists(InvokeMemberExpr addscript, InvokeMemberExpr create |
+ this.asExpr().getExpr() = addscript.getAnArgument() and
+ addscript.getName() = "AddScript" and
+ create.getName() = "Create" and
+
+ addscript.getQualifier().(InvokeMemberExpr) = create and
+ create.getQualifier().(TypeNameExpr).getName() = "PowerShell"
+ )
+ }
+ override string getSinkType(){
+ result = "call to AddScript"
+ }
}
class PowershellSink extends Sink {
@@ -111,7 +112,7 @@ class PowershellSink extends Sink {
c.getName() = "powershell" |
(
this.asExpr().getExpr() = c.getArgument(1) and
- c.getArgument(0).getValue().toString() = "-command"
+ c.getArgument(0).getValue().asString() = "-command"
) or
(
this.asExpr().getExpr() = c.getArgument(0)
@@ -128,7 +129,7 @@ class CmdSink extends Sink {
exists(CmdCall c |
this.asExpr().getExpr() = c.getArgument(1) and
c.getName() = "cmd" and
- c.getArgument(0).getValue().toString() = "/c"
+ c.getArgument(0).getValue().asString() = "/c"
)
}
override string getSinkType(){
@@ -165,7 +166,7 @@ class CreateScriptBlockSink extends Sink {
exists(InvokeMemberExpr ie |
this.asExpr().getExpr() = ie.getAnArgument() and
ie.getName() = "Create" and
- ie.getQualifier().toString() = "ScriptBlock"
+ ie.getQualifier().(TypeNameExpr).getName() = "ScriptBlock"
)
}
override string getSinkType(){
@@ -219,9 +220,10 @@ class ExpandStringSink extends Sink {
class SingleQuoteSanitizer extends Sanitizer {
SingleQuoteSanitizer() {
- exists(Expr e, VarReadAccess v |
- e = this.asExpr().getExpr().getParent() and
- e.toString().matches("%'$" + v.getVariable().getName() + "'%")
+ exists(ExpandableStringExpr e, VarReadAccess v |
+ v = this.asExpr().getExpr() and
+ e.getUnexpandedValue().matches("%'$" + v.getVariable().getName() + "'%") and
+ e.getAnExpr() = v
)
}
}
diff --git a/powershell/ql/test/query-tests/security/cwe-078/CommandInjection/CommandInjection.expected b/powershell/ql/test/query-tests/security/cwe-078/CommandInjection/CommandInjection.expected
index 144255ccc3c..e8e97671e55 100644
--- a/powershell/ql/test/query-tests/security/cwe-078/CommandInjection/CommandInjection.expected
+++ b/powershell/ql/test/query-tests/security/cwe-078/CommandInjection/CommandInjection.expected
@@ -1,12 +1,137 @@
edges
-| test.ps1:1:8:1:9 | x | test.ps1:3:28:3:47 | Get-Process -Id $x | provenance | |
-| test.ps1:5:10:5:20 | my_var | test.ps1:7:3:7:19 | $code --enabled | provenance | |
+| test.ps1:3:11:3:20 | UserInput | test.ps1:4:23:4:52 | Get-Process -Name $UserInput | provenance | |
+| test.ps1:9:11:9:20 | UserInput | test.ps1:10:9:10:38 | Get-Process -Name $UserInput | provenance | |
+| test.ps1:15:11:15:20 | UserInput | test.ps1:16:50:16:79 | Get-Process -Name $UserInput | provenance | |
+| test.ps1:21:11:21:20 | UserInput | test.ps1:22:41:22:70 | Get-Process -Name $UserInput | provenance | |
+| test.ps1:21:11:21:20 | UserInput | test.ps1:22:60:22:69 | UserInput | provenance | |
+| test.ps1:27:11:27:20 | UserInput | test.ps1:28:38:28:67 | Get-Process -Name $UserInput | provenance | |
+| test.ps1:27:11:27:20 | UserInput | test.ps1:28:57:28:66 | UserInput | provenance | |
+| test.ps1:33:11:33:20 | UserInput | test.ps1:34:14:34:46 | public class Foo { $UserInput } | provenance | |
+| test.ps1:39:11:39:20 | UserInput | test.ps1:40:30:40:62 | public class Foo { $UserInput } | provenance | |
+| test.ps1:45:11:45:20 | UserInput | test.ps1:48:30:48:34 | code | provenance | |
+| test.ps1:73:11:73:20 | UserInput | test.ps1:75:25:75:54 | Get-Process -Name $UserInput | provenance | |
+| test.ps1:80:11:80:20 | UserInput | test.ps1:82:16:82:45 | Get-Process -Name $UserInput | provenance | |
+| test.ps1:87:11:87:20 | UserInput | test.ps1:89:12:89:28 | ping $UserInput | provenance | |
+| test.ps1:94:11:94:20 | UserInput | test.ps1:98:33:98:62 | Get-Process -Name $UserInput | provenance | |
+| test.ps1:104:11:104:20 | UserInput | test.ps1:108:58:108:87 | Get-Process -Name $UserInput | provenance | |
+| test.ps1:114:11:114:20 | UserInput | test.ps1:116:34:116:43 | UserInput | provenance | |
+| test.ps1:121:11:121:20 | UserInput | test.ps1:123:28:123:37 | UserInput | provenance | |
+| test.ps1:128:11:128:20 | UserInput | test.ps1:130:28:130:37 | UserInput | provenance | |
+| test.ps1:136:11:136:20 | UserInput | test.ps1:139:50:139:59 | UserInput | provenance | |
+| test.ps1:144:11:144:20 | UserInput | test.ps1:147:63:147:72 | UserInput | provenance | |
+| test.ps1:152:10:152:32 | Call to Read-Host | test.ps1:154:46:154:51 | input | provenance | Src:MaD:11464 |
+| test.ps1:152:10:152:32 | Call to Read-Host | test.ps1:155:46:155:51 | input | provenance | Src:MaD:11464 |
+| test.ps1:152:10:152:32 | Call to Read-Host | test.ps1:156:46:156:51 | input | provenance | Src:MaD:11464 |
+| test.ps1:152:10:152:32 | Call to Read-Host | test.ps1:157:46:157:51 | input | provenance | Src:MaD:11464 |
+| test.ps1:152:10:152:32 | Call to Read-Host | test.ps1:158:46:158:51 | input | provenance | Src:MaD:11464 |
+| test.ps1:152:10:152:32 | Call to Read-Host | test.ps1:159:46:159:51 | input | provenance | Src:MaD:11464 |
+| test.ps1:152:10:152:32 | Call to Read-Host | test.ps1:160:46:160:51 | input | provenance | Src:MaD:11464 |
+| test.ps1:152:10:152:32 | Call to Read-Host | test.ps1:161:46:161:51 | input | provenance | Src:MaD:11464 |
+| test.ps1:152:10:152:32 | Call to Read-Host | test.ps1:163:48:163:53 | input | provenance | Src:MaD:11464 |
+| test.ps1:152:10:152:32 | Call to Read-Host | test.ps1:164:48:164:53 | input | provenance | Src:MaD:11464 |
+| test.ps1:152:10:152:32 | Call to Read-Host | test.ps1:165:48:165:53 | input | provenance | Src:MaD:11464 |
+| test.ps1:152:10:152:32 | Call to Read-Host | test.ps1:166:41:166:46 | input | provenance | Src:MaD:11464 |
+| test.ps1:152:10:152:32 | Call to Read-Host | test.ps1:167:41:167:46 | input | provenance | Src:MaD:11464 |
+| test.ps1:152:10:152:32 | Call to Read-Host | test.ps1:168:36:168:41 | input | provenance | Src:MaD:11464 |
+| test.ps1:152:10:152:32 | Call to Read-Host | test.ps1:169:36:169:41 | input | provenance | Src:MaD:11464 |
+| test.ps1:152:10:152:32 | Call to Read-Host | test.ps1:170:36:170:41 | input | provenance | Src:MaD:11464 |
+| test.ps1:152:10:152:32 | Call to Read-Host | test.ps1:172:42:172:47 | input | provenance | Src:MaD:11464 |
+| test.ps1:152:10:152:32 | Call to Read-Host | test.ps1:173:42:173:47 | input | provenance | Src:MaD:11464 |
+| test.ps1:154:46:154:51 | input | test.ps1:3:11:3:20 | UserInput | provenance | |
+| test.ps1:155:46:155:51 | input | test.ps1:9:11:9:20 | UserInput | provenance | |
+| test.ps1:156:46:156:51 | input | test.ps1:15:11:15:20 | UserInput | provenance | |
+| test.ps1:157:46:157:51 | input | test.ps1:21:11:21:20 | UserInput | provenance | |
+| test.ps1:158:46:158:51 | input | test.ps1:27:11:27:20 | UserInput | provenance | |
+| test.ps1:159:46:159:51 | input | test.ps1:33:11:33:20 | UserInput | provenance | |
+| test.ps1:160:46:160:51 | input | test.ps1:39:11:39:20 | UserInput | provenance | |
+| test.ps1:161:46:161:51 | input | test.ps1:45:11:45:20 | UserInput | provenance | |
+| test.ps1:163:48:163:53 | input | test.ps1:73:11:73:20 | UserInput | provenance | |
+| test.ps1:164:48:164:53 | input | test.ps1:80:11:80:20 | UserInput | provenance | |
+| test.ps1:165:48:165:53 | input | test.ps1:87:11:87:20 | UserInput | provenance | |
+| test.ps1:166:41:166:46 | input | test.ps1:94:11:94:20 | UserInput | provenance | |
+| test.ps1:167:41:167:46 | input | test.ps1:104:11:104:20 | UserInput | provenance | |
+| test.ps1:168:36:168:41 | input | test.ps1:114:11:114:20 | UserInput | provenance | |
+| test.ps1:169:36:169:41 | input | test.ps1:121:11:121:20 | UserInput | provenance | |
+| test.ps1:170:36:170:41 | input | test.ps1:128:11:128:20 | UserInput | provenance | |
+| test.ps1:172:42:172:47 | input | test.ps1:136:11:136:20 | UserInput | provenance | |
+| test.ps1:173:42:173:47 | input | test.ps1:144:11:144:20 | UserInput | provenance | |
nodes
-| test.ps1:1:8:1:9 | x | semmle.label | x |
-| test.ps1:3:28:3:47 | Get-Process -Id $x | semmle.label | Get-Process -Id $x |
-| test.ps1:5:10:5:20 | my_var | semmle.label | my_var |
-| test.ps1:7:3:7:19 | $code --enabled | semmle.label | $code --enabled |
+| test.ps1:3:11:3:20 | UserInput | semmle.label | UserInput |
+| test.ps1:4:23:4:52 | Get-Process -Name $UserInput | semmle.label | Get-Process -Name $UserInput |
+| test.ps1:9:11:9:20 | UserInput | semmle.label | UserInput |
+| test.ps1:10:9:10:38 | Get-Process -Name $UserInput | semmle.label | Get-Process -Name $UserInput |
+| test.ps1:15:11:15:20 | UserInput | semmle.label | UserInput |
+| test.ps1:16:50:16:79 | Get-Process -Name $UserInput | semmle.label | Get-Process -Name $UserInput |
+| test.ps1:21:11:21:20 | UserInput | semmle.label | UserInput |
+| test.ps1:22:41:22:70 | Get-Process -Name $UserInput | semmle.label | Get-Process -Name $UserInput |
+| test.ps1:22:60:22:69 | UserInput | semmle.label | UserInput |
+| test.ps1:27:11:27:20 | UserInput | semmle.label | UserInput |
+| test.ps1:28:38:28:67 | Get-Process -Name $UserInput | semmle.label | Get-Process -Name $UserInput |
+| test.ps1:28:57:28:66 | UserInput | semmle.label | UserInput |
+| test.ps1:33:11:33:20 | UserInput | semmle.label | UserInput |
+| test.ps1:34:14:34:46 | public class Foo { $UserInput } | semmle.label | public class Foo { $UserInput } |
+| test.ps1:39:11:39:20 | UserInput | semmle.label | UserInput |
+| test.ps1:40:30:40:62 | public class Foo { $UserInput } | semmle.label | public class Foo { $UserInput } |
+| test.ps1:45:11:45:20 | UserInput | semmle.label | UserInput |
+| test.ps1:48:30:48:34 | code | semmle.label | code |
+| test.ps1:73:11:73:20 | UserInput | semmle.label | UserInput |
+| test.ps1:75:25:75:54 | Get-Process -Name $UserInput | semmle.label | Get-Process -Name $UserInput |
+| test.ps1:80:11:80:20 | UserInput | semmle.label | UserInput |
+| test.ps1:82:16:82:45 | Get-Process -Name $UserInput | semmle.label | Get-Process -Name $UserInput |
+| test.ps1:87:11:87:20 | UserInput | semmle.label | UserInput |
+| test.ps1:89:12:89:28 | ping $UserInput | semmle.label | ping $UserInput |
+| test.ps1:94:11:94:20 | UserInput | semmle.label | UserInput |
+| test.ps1:98:33:98:62 | Get-Process -Name $UserInput | semmle.label | Get-Process -Name $UserInput |
+| test.ps1:104:11:104:20 | UserInput | semmle.label | UserInput |
+| test.ps1:108:58:108:87 | Get-Process -Name $UserInput | semmle.label | Get-Process -Name $UserInput |
+| test.ps1:114:11:114:20 | UserInput | semmle.label | UserInput |
+| test.ps1:116:34:116:43 | UserInput | semmle.label | UserInput |
+| test.ps1:121:11:121:20 | UserInput | semmle.label | UserInput |
+| test.ps1:123:28:123:37 | UserInput | semmle.label | UserInput |
+| test.ps1:128:11:128:20 | UserInput | semmle.label | UserInput |
+| test.ps1:130:28:130:37 | UserInput | semmle.label | UserInput |
+| test.ps1:136:11:136:20 | UserInput | semmle.label | UserInput |
+| test.ps1:139:50:139:59 | UserInput | semmle.label | UserInput |
+| test.ps1:144:11:144:20 | UserInput | semmle.label | UserInput |
+| test.ps1:147:63:147:72 | UserInput | semmle.label | UserInput |
+| test.ps1:152:10:152:32 | Call to Read-Host | semmle.label | Call to Read-Host |
+| test.ps1:154:46:154:51 | input | semmle.label | input |
+| test.ps1:155:46:155:51 | input | semmle.label | input |
+| test.ps1:156:46:156:51 | input | semmle.label | input |
+| test.ps1:157:46:157:51 | input | semmle.label | input |
+| test.ps1:158:46:158:51 | input | semmle.label | input |
+| test.ps1:159:46:159:51 | input | semmle.label | input |
+| test.ps1:160:46:160:51 | input | semmle.label | input |
+| test.ps1:161:46:161:51 | input | semmle.label | input |
+| test.ps1:163:48:163:53 | input | semmle.label | input |
+| test.ps1:164:48:164:53 | input | semmle.label | input |
+| test.ps1:165:48:165:53 | input | semmle.label | input |
+| test.ps1:166:41:166:46 | input | semmle.label | input |
+| test.ps1:167:41:167:46 | input | semmle.label | input |
+| test.ps1:168:36:168:41 | input | semmle.label | input |
+| test.ps1:169:36:169:41 | input | semmle.label | input |
+| test.ps1:170:36:170:41 | input | semmle.label | input |
+| test.ps1:172:42:172:47 | input | semmle.label | input |
+| test.ps1:173:42:173:47 | input | semmle.label | input |
subpaths
#select
-| test.ps1:3:28:3:47 | Get-Process -Id $x | test.ps1:1:8:1:9 | x | test.ps1:3:28:3:47 | Get-Process -Id $x | This command depends on a $@. | test.ps1:1:8:1:9 | x | user-provided value |
-| test.ps1:7:3:7:19 | $code --enabled | test.ps1:5:10:5:20 | my_var | test.ps1:7:3:7:19 | $code --enabled | This command depends on a $@. | test.ps1:5:10:5:20 | my_var | user-provided value |
+| test.ps1:4:23:4:52 | Get-Process -Name $UserInput | test.ps1:152:10:152:32 | Call to Read-Host | test.ps1:4:23:4:52 | Get-Process -Name $UserInput | This command depends on a $@. | test.ps1:152:10:152:32 | Call to Read-Host | user-provided value |
+| test.ps1:10:9:10:38 | Get-Process -Name $UserInput | test.ps1:152:10:152:32 | Call to Read-Host | test.ps1:10:9:10:38 | Get-Process -Name $UserInput | This command depends on a $@. | test.ps1:152:10:152:32 | Call to Read-Host | user-provided value |
+| test.ps1:16:50:16:79 | Get-Process -Name $UserInput | test.ps1:152:10:152:32 | Call to Read-Host | test.ps1:16:50:16:79 | Get-Process -Name $UserInput | This command depends on a $@. | test.ps1:152:10:152:32 | Call to Read-Host | user-provided value |
+| test.ps1:22:41:22:70 | Get-Process -Name $UserInput | test.ps1:152:10:152:32 | Call to Read-Host | test.ps1:22:41:22:70 | Get-Process -Name $UserInput | This command depends on a $@. | test.ps1:152:10:152:32 | Call to Read-Host | user-provided value |
+| test.ps1:22:60:22:69 | UserInput | test.ps1:152:10:152:32 | Call to Read-Host | test.ps1:22:60:22:69 | UserInput | This command depends on a $@. | test.ps1:152:10:152:32 | Call to Read-Host | user-provided value |
+| test.ps1:28:38:28:67 | Get-Process -Name $UserInput | test.ps1:152:10:152:32 | Call to Read-Host | test.ps1:28:38:28:67 | Get-Process -Name $UserInput | This command depends on a $@. | test.ps1:152:10:152:32 | Call to Read-Host | user-provided value |
+| test.ps1:28:57:28:66 | UserInput | test.ps1:152:10:152:32 | Call to Read-Host | test.ps1:28:57:28:66 | UserInput | This command depends on a $@. | test.ps1:152:10:152:32 | Call to Read-Host | user-provided value |
+| test.ps1:34:14:34:46 | public class Foo { $UserInput } | test.ps1:152:10:152:32 | Call to Read-Host | test.ps1:34:14:34:46 | public class Foo { $UserInput } | This command depends on a $@. | test.ps1:152:10:152:32 | Call to Read-Host | user-provided value |
+| test.ps1:40:30:40:62 | public class Foo { $UserInput } | test.ps1:152:10:152:32 | Call to Read-Host | test.ps1:40:30:40:62 | public class Foo { $UserInput } | This command depends on a $@. | test.ps1:152:10:152:32 | Call to Read-Host | user-provided value |
+| test.ps1:48:30:48:34 | code | test.ps1:152:10:152:32 | Call to Read-Host | test.ps1:48:30:48:34 | code | This command depends on a $@. | test.ps1:152:10:152:32 | Call to Read-Host | user-provided value |
+| test.ps1:75:25:75:54 | Get-Process -Name $UserInput | test.ps1:152:10:152:32 | Call to Read-Host | test.ps1:75:25:75:54 | Get-Process -Name $UserInput | This command depends on a $@. | test.ps1:152:10:152:32 | Call to Read-Host | user-provided value |
+| test.ps1:82:16:82:45 | Get-Process -Name $UserInput | test.ps1:152:10:152:32 | Call to Read-Host | test.ps1:82:16:82:45 | Get-Process -Name $UserInput | This command depends on a $@. | test.ps1:152:10:152:32 | Call to Read-Host | user-provided value |
+| test.ps1:89:12:89:28 | ping $UserInput | test.ps1:152:10:152:32 | Call to Read-Host | test.ps1:89:12:89:28 | ping $UserInput | This command depends on a $@. | test.ps1:152:10:152:32 | Call to Read-Host | user-provided value |
+| test.ps1:98:33:98:62 | Get-Process -Name $UserInput | test.ps1:152:10:152:32 | Call to Read-Host | test.ps1:98:33:98:62 | Get-Process -Name $UserInput | This command depends on a $@. | test.ps1:152:10:152:32 | Call to Read-Host | user-provided value |
+| test.ps1:108:58:108:87 | Get-Process -Name $UserInput | test.ps1:152:10:152:32 | Call to Read-Host | test.ps1:108:58:108:87 | Get-Process -Name $UserInput | This command depends on a $@. | test.ps1:152:10:152:32 | Call to Read-Host | user-provided value |
+| test.ps1:116:34:116:43 | UserInput | test.ps1:152:10:152:32 | Call to Read-Host | test.ps1:116:34:116:43 | UserInput | This command depends on a $@. | test.ps1:152:10:152:32 | Call to Read-Host | user-provided value |
+| test.ps1:123:28:123:37 | UserInput | test.ps1:152:10:152:32 | Call to Read-Host | test.ps1:123:28:123:37 | UserInput | This command depends on a $@. | test.ps1:152:10:152:32 | Call to Read-Host | user-provided value |
+| test.ps1:130:28:130:37 | UserInput | test.ps1:152:10:152:32 | Call to Read-Host | test.ps1:130:28:130:37 | UserInput | This command depends on a $@. | test.ps1:152:10:152:32 | Call to Read-Host | user-provided value |
+| test.ps1:139:50:139:59 | UserInput | test.ps1:152:10:152:32 | Call to Read-Host | test.ps1:139:50:139:59 | UserInput | This command depends on a $@. | test.ps1:152:10:152:32 | Call to Read-Host | user-provided value |
+| test.ps1:147:63:147:72 | UserInput | test.ps1:152:10:152:32 | Call to Read-Host | test.ps1:147:63:147:72 | UserInput | This command depends on a $@. | test.ps1:152:10:152:32 | Call to Read-Host | user-provided value |