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 |