diff --git a/powershell/ql/lib/semmle/code/powershell/ApiGraphs.qll b/powershell/ql/lib/semmle/code/powershell/ApiGraphs.qll index 7dc12ef10548..5e04b6f75795 100644 --- a/powershell/ql/lib/semmle/code/powershell/ApiGraphs.qll +++ b/powershell/ql/lib/semmle/code/powershell/ApiGraphs.qll @@ -249,6 +249,15 @@ module API { result = this.getContent(contents.getAReadContent()) } + /** + * Gets a representative for the instanceof field of the given `name`. + */ + pragma[inline] + Node getField(string name) { + // This predicate is currently not 'inline_late' because 'name' can be an input or output + Impl::fieldEdge(this.getAnEpsilonSuccessor(), name, result) + } + /** * Gets a representative for an arbitrary element of this collection. */ @@ -615,6 +624,15 @@ module API { contentEdge(pred, any(DataFlow::ContentSet set | set.isAnyElement()).getAReadContent(), succ) } + cached + predicate fieldEdge(Node pred, string name, Node succ) { + exists(DataFlow::ContentSet set, DataFlow::Content::FieldContent fc | + fc.getLowerCaseName() = name and + set.isSingleton(fc) and + contentEdge(pred, set.getAReadContent(), succ) + ) + } + cached predicate parameterEdge(Node pred, DataFlowDispatch::ParameterPosition paramPos, Node succ) { exists(DataFlowPrivate::ParameterNodeImpl parameter, DataFlow::CallableNode callable | diff --git a/powershell/ql/lib/semmle/code/powershell/controlflow/CfgNodes.qll b/powershell/ql/lib/semmle/code/powershell/controlflow/CfgNodes.qll index 4716cc746042..8a3589afea8f 100644 --- a/powershell/ql/lib/semmle/code/powershell/controlflow/CfgNodes.qll +++ b/powershell/ql/lib/semmle/code/powershell/controlflow/CfgNodes.qll @@ -72,8 +72,25 @@ abstract private class ChildMapping extends Ast { */ abstract predicate relevantChild(Ast child); + /** + * Holds if `child` appears before its parent in the control-flow graph. + * This always holds for expressions, and _almost_ never for statements. + */ + abstract predicate precedesParent(Ast child); + pragma[nomagic] - abstract predicate reachesBasicBlock(Ast child, CfgNode cfn, BasicBlock bb); + final predicate reachesBasicBlock(Ast child, CfgNode cfn, BasicBlock bb) { + this.relevantChild(child) and + cfn.getAstNode() = this and + bb.getANode() = cfn + or + exists(BasicBlock mid | + this.reachesBasicBlock(child, cfn, mid) and + not mid.getANode().getAstNode() = child + | + if this.precedesParent(child) then bb = mid.getAPredecessor() else bb = mid.getASuccessor() + ) + } /** * Holds if there is a control-flow path from `cfn` to `cfnChild`, where `cfn` @@ -93,18 +110,7 @@ abstract private class ChildMapping extends Ast { * A class for mapping parent-child AST nodes to parent-child CFG nodes. */ abstract private class ExprChildMapping extends Expr, ChildMapping { - pragma[nomagic] - override predicate reachesBasicBlock(Ast child, CfgNode cfn, BasicBlock bb) { - this.relevantChild(child) and - cfn.getAstNode() = this and - bb.getANode() = cfn - or - exists(BasicBlock mid | - this.reachesBasicBlock(child, cfn, mid) and - bb = mid.getAPredecessor() and - not mid.getANode().getAstNode() = child - ) - } + final override predicate precedesParent(Ast child) { this.relevantChild(child) } } /** @@ -113,18 +119,7 @@ abstract private class ExprChildMapping extends Expr, ChildMapping { abstract private class NonExprChildMapping extends ChildMapping { NonExprChildMapping() { not this instanceof Expr } - pragma[nomagic] - override predicate reachesBasicBlock(Ast child, CfgNode cfn, BasicBlock bb) { - this.relevantChild(child) and - cfn.getAstNode() = this and - bb.getANode() = cfn - or - exists(BasicBlock mid | - this.reachesBasicBlock(child, cfn, mid) and - bb = mid.getASuccessor() and - not mid.getANode().getAstNode() = child - ) - } + override predicate precedesParent(Ast child) { none() } // this is not final because it is overriden by ForEachStmt } private class AttributeBaseChildMapping extends NonExprChildMapping, AttributeBase { @@ -1208,7 +1203,7 @@ module StmtNodes { StmtCfgNode getBody() { s.hasCfgChild(s.getBody(), this, result) } } - private class LoopStmtChildMapping extends NonExprChildMapping, LoopStmt { + abstract private class LoopStmtChildMapping extends ChildMapping, LoopStmt { override predicate relevantChild(Ast child) { child = this.getBody() } } @@ -1222,7 +1217,9 @@ module StmtNodes { StmtCfgNode getBody() { s.hasCfgChild(s.getBody(), this, result) } } - private class DoUntilStmtChildMapping extends LoopStmtChildMapping, DoUntilStmt { + private class DoUntilStmtChildMapping extends LoopStmtChildMapping, NonExprChildMapping, + DoUntilStmt + { override predicate relevantChild(Ast child) { child = this.getCondition() or super.relevantChild(child) } @@ -1238,7 +1235,9 @@ module StmtNodes { ExprCfgNode getCondition() { s.hasCfgChild(s.getCondition(), this, result) } } - private class DoWhileStmtChildMapping extends LoopStmtChildMapping, DoWhileStmt { + private class DoWhileStmtChildMapping extends LoopStmtChildMapping, NonExprChildMapping, + DoWhileStmt + { override predicate relevantChild(Ast child) { child = this.getCondition() or super.relevantChild(child) } @@ -1300,10 +1299,14 @@ module StmtNodes { ExprCfgNode getHashTableExpr() { s.hasCfgChild(s.getHashTableExpr(), this, result) } } - private class ForEachStmtChildMapping extends LoopStmtChildMapping, ForEachStmt { + private class ForEachStmtChildMapping extends LoopStmtChildMapping, NonExprChildMapping, + ForEachStmt + { override predicate relevantChild(Ast child) { child = this.getVarAccess() or child = this.getIterableExpr() or super.relevantChild(child) } + + override predicate precedesParent(Ast child) { child = this.getIterableExpr() } } class ForEachStmtCfgNode extends LoopStmtCfgNode { @@ -1313,12 +1316,12 @@ module StmtNodes { override ForEachStmt getStmt() { result = s } - ExprCfgNode getVarAccess() { s.hasCfgChild(s.getVarAccess(), this, result) } + ExprNodes::VarAccessCfgNode getVarAccess() { s.hasCfgChild(s.getVarAccess(), this, result) } ExprCfgNode getIterableExpr() { s.hasCfgChild(s.getIterableExpr(), this, result) } } - private class ForStmtChildMapping extends LoopStmtChildMapping, ForStmt { + private class ForStmtChildMapping extends LoopStmtChildMapping, NonExprChildMapping, ForStmt { override predicate relevantChild(Ast child) { child = this.getInitializer() or child = this.getCondition() or @@ -1476,7 +1479,7 @@ module StmtNodes { override UsingStmt getStmt() { result = s } } - private class WhileStmtChildMapping extends LoopStmtChildMapping, WhileStmt { + private class WhileStmtChildMapping extends LoopStmtChildMapping, NonExprChildMapping, WhileStmt { override predicate relevantChild(Ast child) { child = this.getCondition() or super.relevantChild(child) diff --git a/powershell/ql/lib/semmle/code/powershell/dataflow/internal/DataFlowPrivate.qll b/powershell/ql/lib/semmle/code/powershell/dataflow/internal/DataFlowPrivate.qll index 6309c4d167c2..1d3683b75d2c 100644 --- a/powershell/ql/lib/semmle/code/powershell/dataflow/internal/DataFlowPrivate.qll +++ b/powershell/ql/lib/semmle/code/powershell/dataflow/internal/DataFlowPrivate.qll @@ -1025,6 +1025,7 @@ predicate storeStep(Node node1, ContentSet c, Node node2) { * Holds if there is a read step of content `c` from `node1` to `node2`. */ predicate readStep(Node node1, ContentSet c, Node node2) { + // Qualifier -> Member read exists(CfgNodes::ExprNodes::MemberExprReadAccessCfgNode var, Content::FieldContent fc | node2.asExpr() = var and node1.asExpr() = var.getQualifier() and @@ -1032,6 +1033,7 @@ predicate readStep(Node node1, ContentSet c, Node node2) { c.isSingleton(fc) ) or + // Qualifier -> Index read exists(CfgNodes::ExprNodes::IndexExprReadAccessCfgNode var, CfgNodes::ExprCfgNode e | node2.asExpr() = var and node1.asExpr() = var.getBase() and @@ -1046,18 +1048,21 @@ predicate readStep(Node node1, ContentSet c, Node node2) { c.isAnyElement() ) or + // Implicit read before a return exists(CfgNode cfgNode | node1 = TPreReturnNodeImpl(cfgNode, true) and node2 = TImplicitWrapNode(cfgNode, true) and c.isSingleton(any(Content::KnownElementContent ec | exists(ec.getIndex().asInt()))) ) or + // Implicit read before a process block c.isAnyPositional() and exists(CfgNodes::ProcessBlockCfgNode processBlock | processBlock.getPipelineParameterAccess() = node1.asExpr() and node2 = TProcessNode(processBlock) ) or + // Implicit read of a positional before a property-by-name process iteration c.isAnyPositional() and exists(CfgNodes::ProcessBlockCfgNode pb, CfgNodes::ExprNodes::VarReadAccessCfgNode va | va = pb.getAPipelineByPropertyNameParameterAccess() and @@ -1065,6 +1070,7 @@ predicate readStep(Node node1, ContentSet c, Node node2) { node2 = TProcessPropertyByNameNode(va.getVariable(), false) ) or + // Implicit read of a property before a property-by-name process iteration exists(PipelineByPropertyNameParameter p, Content::KnownElementContent ec | c.isKnownOrUnknownElement(ec) and ec.getIndex().asString() = p.getLowerCaseName() and @@ -1072,6 +1078,18 @@ predicate readStep(Node node1, ContentSet c, Node node2) { node2 = TProcessPropertyByNameNode(p, true) ) or + // Read from a collection into a `foreach` loop + exists( + CfgNodes::StmtNodes::ForEachStmtCfgNode forEach, Content::KnownElementContent ec, BasicBlock bb, + int i + | + c.isKnownOrUnknownElement(ec) and + node1.asExpr() = forEach.getIterableExpr() and + bb.getNode(i) = forEach.getVarAccess() and + node2.asDefinition().definesAt(_, bb, i) + ) + or + // Summary read steps FlowSummaryImpl::Private::Steps::summaryReadStep(node1.(FlowSummaryNode).getSummaryNode(), c, node2.(FlowSummaryNode).getSummaryNode()) } diff --git a/powershell/ql/lib/semmle/code/powershell/dataflow/internal/DataFlowPublic.qll b/powershell/ql/lib/semmle/code/powershell/dataflow/internal/DataFlowPublic.qll index 2959f4583690..3a194aada561 100644 --- a/powershell/ql/lib/semmle/code/powershell/dataflow/internal/DataFlowPublic.qll +++ b/powershell/ql/lib/semmle/code/powershell/dataflow/internal/DataFlowPublic.qll @@ -1,6 +1,7 @@ private import powershell private import DataFlowDispatch private import DataFlowPrivate +private import semmle.code.powershell.dataflow.Ssa private import semmle.code.powershell.typetracking.internal.TypeTrackingImpl private import semmle.code.powershell.ApiGraphs private import semmle.code.powershell.Cfg @@ -13,6 +14,9 @@ class Node extends TNode { /** Gets the expression corresponding to this node, if any. */ CfgNodes::ExprCfgNode asExpr() { result = this.(ExprNode).getExprNode() } + /** Gets the definition corresponding to this node, if any. */ + Ssa::Definition asDefinition() { result = this.(SsaDefinitionNodeImpl).getDefinition() } + ScriptBlock asCallable() { result = this.(CallableNode).asCallableAstNode() } /** Gets the parameter corresponding to this node, if any. */ @@ -477,14 +481,10 @@ module BarrierGuard { * * For example, `[Foo]::new()` or `New-Object Foo`. */ -class ObjectCreationNode extends ExprNode { - CfgNodes::ExprNodes::ObjectCreationCfgNode objectCreation; - - ObjectCreationNode() { this.getExprNode() = objectCreation } +class ObjectCreationNode extends CallNode { + override CfgNodes::ExprNodes::ObjectCreationCfgNode call; - final CfgNodes::ExprNodes::ObjectCreationCfgNode getObjectCreationNode() { - result = objectCreation - } + final CfgNodes::ExprNodes::ObjectCreationCfgNode getObjectCreationNode() { result = call } /** * Gets the node corresponding to the expression that decides which type @@ -493,7 +493,7 @@ class ObjectCreationNode extends ExprNode { * For example, in `[Foo]::new()`, this would be `Foo`, and in * `New-Object Foo`, this would be `Foo`. */ - Node getConstructedTypeNode() { result.asExpr() = objectCreation.getConstructedTypeExpr() } + Node getConstructedTypeNode() { result.asExpr() = call.getConstructedTypeExpr() } bindingset[result] pragma[inline_late] diff --git a/powershell/ql/lib/semmle/code/powershell/frameworks/Microsoft.PowerShell.model.yml b/powershell/ql/lib/semmle/code/powershell/frameworks/Microsoft.PowerShell.model.yml index c058ec978ea7..8a7099f9a34e 100644 --- a/powershell/ql/lib/semmle/code/powershell/frameworks/Microsoft.PowerShell.model.yml +++ b/powershell/ql/lib/semmle/code/powershell/frameworks/Microsoft.PowerShell.model.yml @@ -44,6 +44,9 @@ extensions: - ["microsoft.powershell.utility!", "Method[format-wide]", "Argument[-inputobject,pipeline]", "ReturnValue", "taint"] - ["microsoft.powershell.utility!", "Method[get-unique]", "Argument[-inputobject,pipeline]", "ReturnValue", "taint"] - ["microsoft.powershell.utility!", "Method[join-string]", "Argument[-inputobject,pipeline]", "ReturnValue", "taint"] + - ["microsoft.powershell.management!", "Method[join-path]", "Argument[-path,0]", "ReturnValue", "taint"] + - ["microsoft.powershell.management!", "Method[join-path]", "Argument[-childpath,1]", "ReturnValue", "taint"] + - ["microsoft.powershell.management!", "Method[join-path]", "Argument[-additionalchildpath,2]", "ReturnValue", "taint"] - addsTo: pack: microsoft/powershell-all diff --git a/powershell/ql/lib/semmle/code/powershell/frameworks/System.IO.model.yml b/powershell/ql/lib/semmle/code/powershell/frameworks/System.IO.model.yml index 88d4fb8587a0..85dfd5c90802 100644 --- a/powershell/ql/lib/semmle/code/powershell/frameworks/System.IO.model.yml +++ b/powershell/ql/lib/semmle/code/powershell/frameworks/System.IO.model.yml @@ -30,4 +30,10 @@ extensions: - ["system.io.filestream", "Instance", "file"] - ["system.io.filestream", "Instance", "file-write"] - ["system.io.streamwriter", "Instance", "file-write"] - - ["system.io.streamwriter", "Instance", "file-write"] \ No newline at end of file + - ["system.io.streamwriter", "Instance", "file-write"] + + - addsTo: + pack: microsoft/powershell-all + extensible: summaryModel + data: + - ["system.io.path!", "Method[getfullpath]", "Argument[0]", "ReturnValue", "taint"] \ No newline at end of file diff --git a/powershell/ql/lib/semmle/code/powershell/security/ZipSlipCustomizations.qll b/powershell/ql/lib/semmle/code/powershell/security/ZipSlipCustomizations.qll new file mode 100644 index 000000000000..37898b427e5a --- /dev/null +++ b/powershell/ql/lib/semmle/code/powershell/security/ZipSlipCustomizations.qll @@ -0,0 +1,96 @@ +/** + * Provides default sources, sinks and sanitizers for reasoning about + * zip slip vulnerabilities, as well as extension points for + * adding your own. + */ + +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 + +module ZipSlip { + /** + * A data flow source for zip slip vulnerabilities. + */ + abstract class Source extends DataFlow::Node { + /** Gets a string that describes the type of this flow source. */ + abstract string getSourceType(); + } + + /** + * A data flow sink for zip slip vulnerabilities. + */ + abstract class Sink extends DataFlow::Node { + abstract string getSinkType(); + } + + /** + * A sanitizer for zip slip vulnerabilities. + */ + abstract class Sanitizer extends DataFlow::Node { } + + /** + * Access to the `FullName` property of the archive item + */ + class ArchiveEntryFullName extends Source { + ArchiveEntryFullName() { + this = + API::getTopLevelMember("system") + .getMember("io") + .getMember("compression") + .getMember("zipfile") + .getReturn("openread") + .getMember("entries") + .getAnElement() + .getField("fullname") + .asSource() + } + + override string getSourceType() { + result = "read of System.IO.Compression.ZipArchiveEntry.FullName" + } + } + + /** + * Argument to extract to file extension method + */ + class SinkCompressionExtractToFileArgument extends Sink { + SinkCompressionExtractToFileArgument() { + exists(DataFlow::CallNode call | + call = + API::getTopLevelMember("system") + .getMember("io") + .getMember("compression") + .getMember("zipfileextensions") + .getMember("extracttofile") + .asCall() and + this = call.getArgument(1) + ) + } + + override string getSinkType() { result = "argument to archive extraction" } + } + + class SinkFileOpenArgument extends Sink { + SinkFileOpenArgument() { + exists(DataFlow::CallNode call | + call = + API::getTopLevelMember("system") + .getMember("io") + .getMember("file") + .getMethod(["open", "openwrite", "create"]) + .asCall() and + this = call.getArgument(0) + ) + } + + override string getSinkType() { result = "argument to file opening" } + } + + private class ExternalZipSlipSink extends Sink { + ExternalZipSlipSink() { this = ModelOutput::getASinkNode("zip-slip").asSink() } + + override string getSinkType() { result = "zip slip" } + } +} diff --git a/powershell/ql/lib/semmle/code/powershell/security/ZipSlipQuery.qll b/powershell/ql/lib/semmle/code/powershell/security/ZipSlipQuery.qll new file mode 100644 index 000000000000..fa4d7293233f --- /dev/null +++ b/powershell/ql/lib/semmle/code/powershell/security/ZipSlipQuery.qll @@ -0,0 +1,24 @@ +/** + * Provides a taint tracking configuration for reasoning about + * zip slip (CWE-022). + * + * Note, for performance reasons: only import this file if + * `ZipSlipFlow` is needed, otherwise + * `ZipSlipCustomizations` should be imported instead. + */ + +import powershell +import semmle.code.powershell.dataflow.flowsources.FlowSources +import semmle.code.powershell.dataflow.DataFlow +import semmle.code.powershell.dataflow.TaintTracking +import ZipSlipCustomizations::ZipSlip + +module Config implements DataFlow::ConfigSig { + predicate isSource(DataFlow::Node source) { source instanceof Source } + + predicate isSink(DataFlow::Node sink) { sink instanceof Sink } + + predicate isBarrier(DataFlow::Node node) { node instanceof Sanitizer } +} + +module ZipSlipFlow = TaintTracking::Global; diff --git a/powershell/ql/src/queries/security/cwe-022/ZipSlip.qhelp b/powershell/ql/src/queries/security/cwe-022/ZipSlip.qhelp new file mode 100644 index 000000000000..200beae109e6 --- /dev/null +++ b/powershell/ql/src/queries/security/cwe-022/ZipSlip.qhelp @@ -0,0 +1,82 @@ + + + +

Extracting files from a malicious zip file, or similar type of archive, +is at risk of directory traversal attacks if filenames from the archive are +not properly validated.

+ +

Zip archives contain archive entries representing each file in the archive. These entries +include a file path for the entry, but these file paths are not restricted and may contain +unexpected special elements such as the directory traversal element (..). If these +file paths are used to create a filesystem path, then a file operation may happen in an +unexpected location. This can result in sensitive information being +revealed or deleted, or an attacker being able to influence behavior by modifying unexpected +files.

+ +

For example, if a zip file contains a file entry ..\sneaky-file, and the zip file +is extracted to the directory c:\output, then naively combining the paths would result +in an output file path of c:\output\..\sneaky-file, which would cause the file to be +written to c:\sneaky-file.

+ +
+ + +

Ensure that output paths constructed from zip archive entries are validated to prevent writing +files to unexpected locations.

+ +

The recommended way of writing an output file from a zip archive entry is to conduct the following in sequence:

+ +
    +
  1. Use Path.Combine(destinationDirectory, archiveEntry.FullName) to determine the raw +output path.
  2. +
  3. Use Path.GetFullPath(..) on the raw output path to resolve any directory traversal +elements.
  4. +
  5. Use Path.GetFullPath(destinationDirectory + Path.DirectorySeparatorChar) to +determine the fully resolved path of the destination directory.
  6. +
  7. Validate that the resolved output path StartsWith the resolved destination +directory, aborting if this is not true.
  8. +
+ +

Another alternative is to validate archive entries against a whitelist of expected files.

+ +
+ + +

In this example, a file path taken from a zip archive item entry is combined with a +destination directory. The result is used as the destination file path without verifying that +the result is within the destination directory. If provided with a zip file containing an archive +path like ..\sneaky-file, then this file would be written outside the destination +directory.

+ + + +

To fix this vulnerability, we can instead use the PowerShell command Expand-Archive +which is safe against this vulnerability by default starting from PowerShell 5.0.

+ + + +

If you need to use the lower-level functionality offered by System.IO.Compression.ZipFile +we need to make three changes. Firstly, we need to resolve any directory traversal or other special +characters in the path by using Path.GetFullPath. Secondly, we need to identify the +destination output directory, again using Path.GetFullPath, this time on the output directory. +Finally, we need to ensure that the resolved output starts with the resolved destination directory, and +throw an exception if this is not the case.

+ + + +
+ + +
  • +Snyk: +Zip Slip Vulnerability. +
  • +
  • +OWASP: +Path Traversal. +
  • + +
    +
    \ No newline at end of file diff --git a/powershell/ql/src/queries/security/cwe-022/ZipSlip.ql b/powershell/ql/src/queries/security/cwe-022/ZipSlip.ql new file mode 100644 index 000000000000..58b583ff2757 --- /dev/null +++ b/powershell/ql/src/queries/security/cwe-022/ZipSlip.ql @@ -0,0 +1,23 @@ +/** + * @name Arbitrary file access during archive extraction ("Zip Slip") + * @description Extracting files from a malicious ZIP file, or similar type of archive, without + * validating that the destination file path is within the destination directory + * can allow an attacker to unexpectedly gain access to resources. + * @kind path-problem + * @id ps/zipslip + * @problem.severity error + * @security-severity 7.5 + * @precision high + * @tags security + * external/cwe/cwe-022 + */ + +import powershell +import semmle.code.powershell.security.ZipSlipQuery +import ZipSlipFlow::PathGraph + +from ZipSlipFlow::PathNode source, ZipSlipFlow::PathNode sink +where ZipSlipFlow::flowPath(source, sink) +select source.getNode(), source, sink, + "Unsanitized archive entry, which may contain '..', is used in a $@.", sink.getNode(), + "file system operation" diff --git a/powershell/ql/src/queries/security/cwe-022/examples/ZipSlipBad.ps1 b/powershell/ql/src/queries/security/cwe-022/examples/ZipSlipBad.ps1 new file mode 100644 index 000000000000..b5fac7247c83 --- /dev/null +++ b/powershell/ql/src/queries/security/cwe-022/examples/ZipSlipBad.ps1 @@ -0,0 +1,9 @@ +$zip = [System.IO.Compression.ZipFile]::OpenRead("MyPath\to\archive.zip") + +foreach ($entry in $zip.Entries) { + $targetPath = Join-Path $extractPath $entry.FullName + + # BAD: No validation of $targetPath + [System.IO.Compression.ZipFileExtensions]::ExtractToFile($entry, $targetPath) +} +$zip.Dispose() \ No newline at end of file diff --git a/powershell/ql/src/queries/security/cwe-022/examples/ZipSlipGood1.ps1 b/powershell/ql/src/queries/security/cwe-022/examples/ZipSlipGood1.ps1 new file mode 100644 index 000000000000..c2bec8b258f0 --- /dev/null +++ b/powershell/ql/src/queries/security/cwe-022/examples/ZipSlipGood1.ps1 @@ -0,0 +1 @@ +Expand-Archive -Path "MyPath\to\archive.zip" -DestinationPath $extractPath -Force diff --git a/powershell/ql/src/queries/security/cwe-022/examples/ZipSlipGood2.ps1 b/powershell/ql/src/queries/security/cwe-022/examples/ZipSlipGood2.ps1 new file mode 100644 index 000000000000..56745c7fb49e --- /dev/null +++ b/powershell/ql/src/queries/security/cwe-022/examples/ZipSlipGood2.ps1 @@ -0,0 +1,15 @@ +$zip = [System.IO.Compression.ZipFile]::OpenRead("MyPath\to\archive.zip") + +foreach ($entry in $zip.Entries) { + $targetPath = Join-Path $extractPath $entry.FullName + $fullTargetPath = [System.IO.Path]::GetFullPath($targetPath) + + # GOOD: Validate that the full path is within the intended extraction directory + $extractRoot = [System.IO.Path]::GetFullPath($extractPath) + if ($fullTargetPath.StartsWith($extractRoot)) { + [System.IO.Compression.ZipFileExtensions]::ExtractToFile($entry, $fullTargetPath, $true) + } else { + Write-Warning "Skipping potentially malicious entry: $($entry.FullName)" + } +} +$zip.Dispose() \ No newline at end of file diff --git a/powershell/ql/test/library-tests/controlflow/elements.expected b/powershell/ql/test/library-tests/controlflow/elements.expected new file mode 100644 index 000000000000..b9ac6ca69b41 --- /dev/null +++ b/powershell/ql/test/library-tests/controlflow/elements.expected @@ -0,0 +1,2 @@ +| graph/functions.ps1:29:5:32:5 | forach(... in ...) | graph/functions.ps1:29:14:29:20 | number | graph/functions.ps1:29:25:29:32 | numbers | graph/functions.ps1:29:35:32:5 | {...} | +| graph/loops.ps1:52:5:55:5 | forach(... in ...) | graph/loops.ps1:52:14:52:20 | letter | graph/loops.ps1:52:25:52:36 | letterArray | graph/loops.ps1:53:5:55:5 | {...} | diff --git a/powershell/ql/test/library-tests/controlflow/elements.ql b/powershell/ql/test/library-tests/controlflow/elements.ql new file mode 100644 index 000000000000..0565c50a2b8a --- /dev/null +++ b/powershell/ql/test/library-tests/controlflow/elements.ql @@ -0,0 +1,9 @@ +import semmle.code.powershell.controlflow.CfgNodes +import ExprNodes +import StmtNodes + +query predicate forEach(ForEachStmtCfgNode forEach, ExprCfgNode va, ExprCfgNode iterable, StmtCfgNode body) { + va = forEach.getVarAccess() and + iterable = forEach.getIterableExpr() and + body = forEach.getBody() +} \ No newline at end of file diff --git a/powershell/ql/test/library-tests/dataflow/fields/test.expected b/powershell/ql/test/library-tests/dataflow/fields/test.expected index 5be94e2ee03b..5fad6fe2abaf 100644 --- a/powershell/ql/test/library-tests/dataflow/fields/test.expected +++ b/powershell/ql/test/library-tests/dataflow/fields/test.expected @@ -83,6 +83,10 @@ edges | test.ps1:88:1:88:5 | [post] hash [b] | test.ps1:89:6:89:10 | hash [b] | provenance | | | test.ps1:88:11:88:21 | Call to source | test.ps1:88:1:88:5 | [post] hash [b] | provenance | | | test.ps1:89:6:89:10 | hash [b] | test.ps1:89:6:89:12 | b | provenance | | +| test.ps1:91:9:91:10 | a | test.ps1:92:10:92:11 | a | provenance | | +| test.ps1:91:15:91:36 | ...,... [element 2] | test.ps1:91:9:91:10 | a | provenance | | +| test.ps1:91:21:91:33 | (...) | test.ps1:91:15:91:36 | ...,... [element 2] | provenance | | +| test.ps1:91:22:91:32 | Call to source | test.ps1:91:21:91:33 | (...) | provenance | | nodes | test.ps1:3:1:3:2 | [post] a [f] | semmle.label | [post] a [f] | | test.ps1:3:8:3:17 | Call to source | semmle.label | Call to source | @@ -179,6 +183,11 @@ nodes | test.ps1:88:11:88:21 | Call to source | semmle.label | Call to source | | test.ps1:89:6:89:10 | hash [b] | semmle.label | hash [b] | | test.ps1:89:6:89:12 | b | semmle.label | b | +| test.ps1:91:9:91:10 | a | semmle.label | a | +| test.ps1:91:15:91:36 | ...,... [element 2] | semmle.label | ...,... [element 2] | +| test.ps1:91:21:91:33 | (...) | semmle.label | (...) | +| test.ps1:91:22:91:32 | Call to source | semmle.label | Call to source | +| test.ps1:92:10:92:11 | a | semmle.label | a | subpaths testFailures #select @@ -208,3 +217,4 @@ testFailures | test.ps1:83:6:83:15 | ...[...] | test.ps1:79:7:79:17 | Call to source | test.ps1:83:6:83:15 | ...[...] | $@ | test.ps1:79:7:79:17 | Call to source | Call to source | | test.ps1:87:6:87:15 | ...[...] | test.ps1:79:7:79:17 | Call to source | test.ps1:87:6:87:15 | ...[...] | $@ | test.ps1:79:7:79:17 | Call to source | Call to source | | test.ps1:89:6:89:12 | b | test.ps1:88:11:88:21 | Call to source | test.ps1:89:6:89:12 | b | $@ | test.ps1:88:11:88:21 | Call to source | Call to source | +| test.ps1:92:10:92:11 | a | test.ps1:91:22:91:32 | Call to source | test.ps1:92:10:92:11 | a | $@ | test.ps1:91:22:91:32 | Call to source | Call to source | diff --git a/powershell/ql/test/library-tests/dataflow/fields/test.ps1 b/powershell/ql/test/library-tests/dataflow/fields/test.ps1 index fcd375c0516b..aba83fb0d41f 100644 --- a/powershell/ql/test/library-tests/dataflow/fields/test.ps1 +++ b/powershell/ql/test/library-tests/dataflow/fields/test.ps1 @@ -86,4 +86,8 @@ Sink $hash["b"] # clean $hash["a"] = 0 Sink $hash["a"] # $ SPURIOUS: hasValueFlow=16 $hash.b = Source "17" -Sink $hash.b # $ hasValueFlow=17 \ No newline at end of file +Sink $hash.b # $ hasValueFlow=17 + +foreach($a in 1, 2, (Source "18"), 3) { + Sink $a # $ hasValueFlow=18 +} \ No newline at end of file diff --git a/powershell/ql/test/library-tests/dataflow/mad/flow.expected b/powershell/ql/test/library-tests/dataflow/mad/flow.expected index 0a3a3a249163..8948013f66c1 100644 --- a/powershell/ql/test/library-tests/dataflow/mad/flow.expected +++ b/powershell/ql/test/library-tests/dataflow/mad/flow.expected @@ -1,8 +1,15 @@ models edges +| file://:0:0:0:0 | [summary param] kw(additionalchildpath) in microsoft.powershell.management!;Method[join-path] | file://:0:0:0:0 | [summary] to write: ReturnValue in microsoft.powershell.management!;Method[join-path] | provenance | | +| file://:0:0:0:0 | [summary param] kw(childpath) in microsoft.powershell.management!;Method[join-path] | file://:0:0:0:0 | [summary] to write: ReturnValue in microsoft.powershell.management!;Method[join-path] | provenance | | +| file://:0:0:0:0 | [summary param] kw(path) in microsoft.powershell.management!;Method[join-path] | file://:0:0:0:0 | [summary] to write: ReturnValue in microsoft.powershell.management!;Method[join-path] | provenance | | | file://:0:0:0:0 | [summary param] pipeline in microsoft.powershell.utility!;Method[join-string] [element 0] | file://:0:0:0:0 | [summary] read: Argument[pipeline].Element[?] in microsoft.powershell.utility!;Method[join-string] | provenance | | | file://:0:0:0:0 | [summary param] pipeline in microsoft.powershell.utility!;Method[join-string] [element 1] | file://:0:0:0:0 | [summary] read: Argument[pipeline].Element[?] in microsoft.powershell.utility!;Method[join-string] | provenance | | +| file://:0:0:0:0 | [summary param] pos(0, {}) in microsoft.powershell.management!;Method[join-path] | file://:0:0:0:0 | [summary] to write: ReturnValue in microsoft.powershell.management!;Method[join-path] | provenance | | +| file://:0:0:0:0 | [summary param] pos(0, {}) in system.io.path!;Method[getfullpath] | file://:0:0:0:0 | [summary] to write: ReturnValue in system.io.path!;Method[getfullpath] | provenance | | | file://:0:0:0:0 | [summary param] pos(0, {}) in system.management.automation.language.codegeneration!;Method[escapesinglequotedstringcontent] | file://:0:0:0:0 | [summary] to write: ReturnValue in system.management.automation.language.codegeneration!;Method[escapesinglequotedstringcontent] | provenance | | +| file://:0:0:0:0 | [summary param] pos(1, {}) in microsoft.powershell.management!;Method[join-path] | file://:0:0:0:0 | [summary] to write: ReturnValue in microsoft.powershell.management!;Method[join-path] | provenance | | +| file://:0:0:0:0 | [summary param] pos(2, {}) in microsoft.powershell.management!;Method[join-path] | file://:0:0:0:0 | [summary] to write: ReturnValue in microsoft.powershell.management!;Method[join-path] | provenance | | | file://:0:0:0:0 | [summary] read: Argument[pipeline].Element[?] in microsoft.powershell.utility!;Method[join-string] | file://:0:0:0:0 | [summary] to write: ReturnValue in microsoft.powershell.utility!;Method[join-string] | provenance | | | file://:0:0:0:0 | [summary] read: Argument[pipeline].Element[?] in microsoft.powershell.utility!;Method[join-string] | file://:0:0:0:0 | [summary] to write: ReturnValue in microsoft.powershell.utility!;Method[join-string] | provenance | | | test.ps1:1:1:1:2 | x | test.ps1:2:94:2:95 | x | provenance | | @@ -12,6 +19,13 @@ edges | test.ps1:2:94:2:95 | x | file://:0:0:0:0 | [summary param] pos(0, {}) in system.management.automation.language.codegeneration!;Method[escapesinglequotedstringcontent] | provenance | | | test.ps1:2:94:2:95 | x | test.ps1:2:6:2:96 | Call to escapesinglequotedstringcontent | provenance | | | test.ps1:5:1:5:2 | x | test.ps1:7:6:7:7 | x | provenance | | +| test.ps1:5:1:5:2 | x | test.ps1:10:23:10:24 | x | provenance | | +| test.ps1:5:1:5:2 | x | test.ps1:13:28:13:29 | x | provenance | | +| test.ps1:5:1:5:2 | x | test.ps1:16:38:16:39 | x | provenance | | +| test.ps1:5:1:5:2 | x | test.ps1:19:17:19:18 | x | provenance | | +| test.ps1:5:1:5:2 | x | test.ps1:22:20:22:21 | x | provenance | | +| test.ps1:5:1:5:2 | x | test.ps1:25:23:25:24 | x | provenance | | +| test.ps1:5:1:5:2 | x | test.ps1:28:37:28:38 | x | provenance | | | test.ps1:5:6:5:15 | Call to source | test.ps1:5:1:5:2 | x | provenance | | | test.ps1:6:1:6:2 | y | test.ps1:7:10:7:11 | y | provenance | | | test.ps1:6:6:6:15 | Call to source | test.ps1:6:1:6:2 | y | provenance | | @@ -23,14 +37,56 @@ edges | test.ps1:7:6:7:11 | ...,... [element 1] | test.ps1:7:15:7:25 | Call to join-string | provenance | | | test.ps1:7:10:7:11 | y | test.ps1:7:6:7:11 | ...,... [element 1] | provenance | | | test.ps1:7:15:7:25 | Call to join-string | test.ps1:7:1:7:2 | z | provenance | | +| test.ps1:10:1:10:3 | z1 | test.ps1:11:6:11:8 | z1 | provenance | | +| test.ps1:10:7:10:27 | Call to join-path | test.ps1:10:1:10:3 | z1 | provenance | | +| test.ps1:10:23:10:24 | x | file://:0:0:0:0 | [summary param] kw(path) in microsoft.powershell.management!;Method[join-path] | provenance | | +| test.ps1:10:23:10:24 | x | test.ps1:10:7:10:27 | Call to join-path | provenance | | +| test.ps1:13:1:13:3 | z2 | test.ps1:14:6:14:8 | z2 | provenance | | +| test.ps1:13:7:13:32 | Call to join-path | test.ps1:13:1:13:3 | z2 | provenance | | +| test.ps1:13:28:13:29 | x | file://:0:0:0:0 | [summary param] kw(childpath) in microsoft.powershell.management!;Method[join-path] | provenance | | +| test.ps1:13:28:13:29 | x | test.ps1:13:7:13:32 | Call to join-path | provenance | | +| test.ps1:16:1:16:3 | z3 | test.ps1:17:6:17:8 | z3 | provenance | | +| test.ps1:16:7:16:42 | Call to join-path | test.ps1:16:1:16:3 | z3 | provenance | | +| test.ps1:16:38:16:39 | x | file://:0:0:0:0 | [summary param] kw(additionalchildpath) in microsoft.powershell.management!;Method[join-path] | provenance | | +| test.ps1:16:38:16:39 | x | test.ps1:16:7:16:42 | Call to join-path | provenance | | +| test.ps1:19:1:19:3 | z4 | test.ps1:20:6:20:8 | z4 | provenance | | +| test.ps1:19:7:19:18 | Call to join-path | test.ps1:19:1:19:3 | z4 | provenance | | +| test.ps1:19:17:19:18 | x | file://:0:0:0:0 | [summary param] pos(0, {}) in microsoft.powershell.management!;Method[join-path] | provenance | | +| test.ps1:19:17:19:18 | x | test.ps1:19:7:19:18 | Call to join-path | provenance | | +| test.ps1:22:1:22:3 | z5 | test.ps1:23:6:23:8 | z5 | provenance | | +| test.ps1:22:7:22:21 | Call to join-path | test.ps1:22:1:22:3 | z5 | provenance | | +| test.ps1:22:20:22:21 | x | file://:0:0:0:0 | [summary param] pos(1, {}) in microsoft.powershell.management!;Method[join-path] | provenance | | +| test.ps1:22:20:22:21 | x | test.ps1:22:7:22:21 | Call to join-path | provenance | | +| test.ps1:25:1:25:3 | z6 | test.ps1:26:6:26:8 | z6 | provenance | | +| test.ps1:25:7:25:24 | Call to join-path | test.ps1:25:1:25:3 | z6 | provenance | | +| test.ps1:25:23:25:24 | x | file://:0:0:0:0 | [summary param] pos(2, {}) in microsoft.powershell.management!;Method[join-path] | provenance | | +| test.ps1:25:23:25:24 | x | test.ps1:25:7:25:24 | Call to join-path | provenance | | +| test.ps1:28:1:28:3 | z7 | test.ps1:29:6:29:8 | z7 | provenance | | +| test.ps1:28:7:28:39 | Call to getfullpath | test.ps1:28:1:28:3 | z7 | provenance | | +| test.ps1:28:37:28:38 | x | file://:0:0:0:0 | [summary param] pos(0, {}) in system.io.path!;Method[getfullpath] | provenance | | +| test.ps1:28:37:28:38 | x | test.ps1:28:7:28:39 | Call to getfullpath | provenance | | nodes +| file://:0:0:0:0 | [summary param] kw(additionalchildpath) in microsoft.powershell.management!;Method[join-path] | semmle.label | [summary param] kw(additionalchildpath) in microsoft.powershell.management!;Method[join-path] | +| file://:0:0:0:0 | [summary param] kw(childpath) in microsoft.powershell.management!;Method[join-path] | semmle.label | [summary param] kw(childpath) in microsoft.powershell.management!;Method[join-path] | +| file://:0:0:0:0 | [summary param] kw(path) in microsoft.powershell.management!;Method[join-path] | semmle.label | [summary param] kw(path) in microsoft.powershell.management!;Method[join-path] | | file://:0:0:0:0 | [summary param] pipeline in microsoft.powershell.utility!;Method[join-string] [element 0] | semmle.label | [summary param] pipeline in microsoft.powershell.utility!;Method[join-string] [element 0] | | file://:0:0:0:0 | [summary param] pipeline in microsoft.powershell.utility!;Method[join-string] [element 1] | semmle.label | [summary param] pipeline in microsoft.powershell.utility!;Method[join-string] [element 1] | +| file://:0:0:0:0 | [summary param] pos(0, {}) in microsoft.powershell.management!;Method[join-path] | semmle.label | [summary param] pos(0, {}) in microsoft.powershell.management!;Method[join-path] | +| file://:0:0:0:0 | [summary param] pos(0, {}) in system.io.path!;Method[getfullpath] | semmle.label | [summary param] pos(0, {}) in system.io.path!;Method[getfullpath] | | file://:0:0:0:0 | [summary param] pos(0, {}) in system.management.automation.language.codegeneration!;Method[escapesinglequotedstringcontent] | semmle.label | [summary param] pos(0, {}) in system.management.automation.language.codegeneration!;Method[escapesinglequotedstringcontent] | +| file://:0:0:0:0 | [summary param] pos(1, {}) in microsoft.powershell.management!;Method[join-path] | semmle.label | [summary param] pos(1, {}) in microsoft.powershell.management!;Method[join-path] | +| file://:0:0:0:0 | [summary param] pos(2, {}) in microsoft.powershell.management!;Method[join-path] | semmle.label | [summary param] pos(2, {}) in microsoft.powershell.management!;Method[join-path] | | file://:0:0:0:0 | [summary] read: Argument[pipeline].Element[?] in microsoft.powershell.utility!;Method[join-string] | semmle.label | [summary] read: Argument[pipeline].Element[?] in microsoft.powershell.utility!;Method[join-string] | | file://:0:0:0:0 | [summary] read: Argument[pipeline].Element[?] in microsoft.powershell.utility!;Method[join-string] | semmle.label | [summary] read: Argument[pipeline].Element[?] in microsoft.powershell.utility!;Method[join-string] | +| file://:0:0:0:0 | [summary] to write: ReturnValue in microsoft.powershell.management!;Method[join-path] | semmle.label | [summary] to write: ReturnValue in microsoft.powershell.management!;Method[join-path] | +| file://:0:0:0:0 | [summary] to write: ReturnValue in microsoft.powershell.management!;Method[join-path] | semmle.label | [summary] to write: ReturnValue in microsoft.powershell.management!;Method[join-path] | +| file://:0:0:0:0 | [summary] to write: ReturnValue in microsoft.powershell.management!;Method[join-path] | semmle.label | [summary] to write: ReturnValue in microsoft.powershell.management!;Method[join-path] | +| file://:0:0:0:0 | [summary] to write: ReturnValue in microsoft.powershell.management!;Method[join-path] | semmle.label | [summary] to write: ReturnValue in microsoft.powershell.management!;Method[join-path] | +| file://:0:0:0:0 | [summary] to write: ReturnValue in microsoft.powershell.management!;Method[join-path] | semmle.label | [summary] to write: ReturnValue in microsoft.powershell.management!;Method[join-path] | +| file://:0:0:0:0 | [summary] to write: ReturnValue in microsoft.powershell.management!;Method[join-path] | semmle.label | [summary] to write: ReturnValue in microsoft.powershell.management!;Method[join-path] | | file://:0:0:0:0 | [summary] to write: ReturnValue in microsoft.powershell.utility!;Method[join-string] | semmle.label | [summary] to write: ReturnValue in microsoft.powershell.utility!;Method[join-string] | | file://:0:0:0:0 | [summary] to write: ReturnValue in microsoft.powershell.utility!;Method[join-string] | semmle.label | [summary] to write: ReturnValue in microsoft.powershell.utility!;Method[join-string] | +| file://:0:0:0:0 | [summary] to write: ReturnValue in system.io.path!;Method[getfullpath] | semmle.label | [summary] to write: ReturnValue in system.io.path!;Method[getfullpath] | | file://:0:0:0:0 | [summary] to write: ReturnValue in system.management.automation.language.codegeneration!;Method[escapesinglequotedstringcontent] | semmle.label | [summary] to write: ReturnValue in system.management.automation.language.codegeneration!;Method[escapesinglequotedstringcontent] | | test.ps1:1:1:1:2 | x | semmle.label | x | | test.ps1:1:6:1:15 | Call to source | semmle.label | Call to source | @@ -49,12 +105,54 @@ nodes | test.ps1:7:10:7:11 | y | semmle.label | y | | test.ps1:7:15:7:25 | Call to join-string | semmle.label | Call to join-string | | test.ps1:8:6:8:7 | z | semmle.label | z | +| test.ps1:10:1:10:3 | z1 | semmle.label | z1 | +| test.ps1:10:7:10:27 | Call to join-path | semmle.label | Call to join-path | +| test.ps1:10:23:10:24 | x | semmle.label | x | +| test.ps1:11:6:11:8 | z1 | semmle.label | z1 | +| test.ps1:13:1:13:3 | z2 | semmle.label | z2 | +| test.ps1:13:7:13:32 | Call to join-path | semmle.label | Call to join-path | +| test.ps1:13:28:13:29 | x | semmle.label | x | +| test.ps1:14:6:14:8 | z2 | semmle.label | z2 | +| test.ps1:16:1:16:3 | z3 | semmle.label | z3 | +| test.ps1:16:7:16:42 | Call to join-path | semmle.label | Call to join-path | +| test.ps1:16:38:16:39 | x | semmle.label | x | +| test.ps1:17:6:17:8 | z3 | semmle.label | z3 | +| test.ps1:19:1:19:3 | z4 | semmle.label | z4 | +| test.ps1:19:7:19:18 | Call to join-path | semmle.label | Call to join-path | +| test.ps1:19:17:19:18 | x | semmle.label | x | +| test.ps1:20:6:20:8 | z4 | semmle.label | z4 | +| test.ps1:22:1:22:3 | z5 | semmle.label | z5 | +| test.ps1:22:7:22:21 | Call to join-path | semmle.label | Call to join-path | +| test.ps1:22:20:22:21 | x | semmle.label | x | +| test.ps1:23:6:23:8 | z5 | semmle.label | z5 | +| test.ps1:25:1:25:3 | z6 | semmle.label | z6 | +| test.ps1:25:7:25:24 | Call to join-path | semmle.label | Call to join-path | +| test.ps1:25:23:25:24 | x | semmle.label | x | +| test.ps1:26:6:26:8 | z6 | semmle.label | z6 | +| test.ps1:28:1:28:3 | z7 | semmle.label | z7 | +| test.ps1:28:7:28:39 | Call to getfullpath | semmle.label | Call to getfullpath | +| test.ps1:28:37:28:38 | x | semmle.label | x | +| test.ps1:29:6:29:8 | z7 | semmle.label | z7 | subpaths | test.ps1:2:94:2:95 | x | file://:0:0:0:0 | [summary param] pos(0, {}) in system.management.automation.language.codegeneration!;Method[escapesinglequotedstringcontent] | file://:0:0:0:0 | [summary] to write: ReturnValue in system.management.automation.language.codegeneration!;Method[escapesinglequotedstringcontent] | test.ps1:2:6:2:96 | Call to escapesinglequotedstringcontent | | test.ps1:7:6:7:11 | ...,... [element 0] | file://:0:0:0:0 | [summary param] pipeline in microsoft.powershell.utility!;Method[join-string] [element 0] | file://:0:0:0:0 | [summary] to write: ReturnValue in microsoft.powershell.utility!;Method[join-string] | test.ps1:7:15:7:25 | Call to join-string | | test.ps1:7:6:7:11 | ...,... [element 1] | file://:0:0:0:0 | [summary param] pipeline in microsoft.powershell.utility!;Method[join-string] [element 1] | file://:0:0:0:0 | [summary] to write: ReturnValue in microsoft.powershell.utility!;Method[join-string] | test.ps1:7:15:7:25 | Call to join-string | +| test.ps1:10:23:10:24 | x | file://:0:0:0:0 | [summary param] kw(path) in microsoft.powershell.management!;Method[join-path] | file://:0:0:0:0 | [summary] to write: ReturnValue in microsoft.powershell.management!;Method[join-path] | test.ps1:10:7:10:27 | Call to join-path | +| test.ps1:13:28:13:29 | x | file://:0:0:0:0 | [summary param] kw(childpath) in microsoft.powershell.management!;Method[join-path] | file://:0:0:0:0 | [summary] to write: ReturnValue in microsoft.powershell.management!;Method[join-path] | test.ps1:13:7:13:32 | Call to join-path | +| test.ps1:16:38:16:39 | x | file://:0:0:0:0 | [summary param] kw(additionalchildpath) in microsoft.powershell.management!;Method[join-path] | file://:0:0:0:0 | [summary] to write: ReturnValue in microsoft.powershell.management!;Method[join-path] | test.ps1:16:7:16:42 | Call to join-path | +| test.ps1:19:17:19:18 | x | file://:0:0:0:0 | [summary param] pos(0, {}) in microsoft.powershell.management!;Method[join-path] | file://:0:0:0:0 | [summary] to write: ReturnValue in microsoft.powershell.management!;Method[join-path] | test.ps1:19:7:19:18 | Call to join-path | +| test.ps1:22:20:22:21 | x | file://:0:0:0:0 | [summary param] pos(1, {}) in microsoft.powershell.management!;Method[join-path] | file://:0:0:0:0 | [summary] to write: ReturnValue in microsoft.powershell.management!;Method[join-path] | test.ps1:22:7:22:21 | Call to join-path | +| test.ps1:25:23:25:24 | x | file://:0:0:0:0 | [summary param] pos(2, {}) in microsoft.powershell.management!;Method[join-path] | file://:0:0:0:0 | [summary] to write: ReturnValue in microsoft.powershell.management!;Method[join-path] | test.ps1:25:7:25:24 | Call to join-path | +| test.ps1:28:37:28:38 | x | file://:0:0:0:0 | [summary param] pos(0, {}) in system.io.path!;Method[getfullpath] | file://:0:0:0:0 | [summary] to write: ReturnValue in system.io.path!;Method[getfullpath] | test.ps1:28:7:28:39 | Call to getfullpath | testFailures #select | test.ps1:3:6:3:7 | y | test.ps1:1:6:1:15 | Call to source | test.ps1:3:6:3:7 | y | $@ | test.ps1:1:6:1:15 | Call to source | Call to source | | test.ps1:8:6:8:7 | z | test.ps1:5:6:5:15 | Call to source | test.ps1:8:6:8:7 | z | $@ | test.ps1:5:6:5:15 | Call to source | Call to source | | test.ps1:8:6:8:7 | z | test.ps1:6:6:6:15 | Call to source | test.ps1:8:6:8:7 | z | $@ | test.ps1:6:6:6:15 | Call to source | Call to source | +| test.ps1:11:6:11:8 | z1 | test.ps1:5:6:5:15 | Call to source | test.ps1:11:6:11:8 | z1 | $@ | test.ps1:5:6:5:15 | Call to source | Call to source | +| test.ps1:14:6:14:8 | z2 | test.ps1:5:6:5:15 | Call to source | test.ps1:14:6:14:8 | z2 | $@ | test.ps1:5:6:5:15 | Call to source | Call to source | +| test.ps1:17:6:17:8 | z3 | test.ps1:5:6:5:15 | Call to source | test.ps1:17:6:17:8 | z3 | $@ | test.ps1:5:6:5:15 | Call to source | Call to source | +| test.ps1:20:6:20:8 | z4 | test.ps1:5:6:5:15 | Call to source | test.ps1:20:6:20:8 | z4 | $@ | test.ps1:5:6:5:15 | Call to source | Call to source | +| test.ps1:23:6:23:8 | z5 | test.ps1:5:6:5:15 | Call to source | test.ps1:23:6:23:8 | z5 | $@ | test.ps1:5:6:5:15 | Call to source | Call to source | +| test.ps1:26:6:26:8 | z6 | test.ps1:5:6:5:15 | Call to source | test.ps1:26:6:26:8 | z6 | $@ | test.ps1:5:6:5:15 | Call to source | Call to source | +| test.ps1:29:6:29:8 | z7 | test.ps1:5:6:5:15 | Call to source | test.ps1:29:6:29:8 | z7 | $@ | test.ps1:5:6:5:15 | Call to source | Call to source | diff --git a/powershell/ql/test/library-tests/dataflow/mad/test.ps1 b/powershell/ql/test/library-tests/dataflow/mad/test.ps1 index d45af8dcc34d..bada37c73925 100644 --- a/powershell/ql/test/library-tests/dataflow/mad/test.ps1 +++ b/powershell/ql/test/library-tests/dataflow/mad/test.ps1 @@ -5,4 +5,25 @@ Sink $y # $ hasTaintFlow=1 $x = Source "2" $y = Source "3" $z = $x, $y | Join-String -Sink $z # $ hasTaintFlow=2 hasTaintFlow=3 \ No newline at end of file +Sink $z # $ hasTaintFlow=2 hasTaintFlow=3 + +$z1 = Join-Path -Path $x "" +Sink $z1 # $ hasTaintFlow=2 + +$z2 = Join-Path -ChildPath $x "" +Sink $z2 # $ hasTaintFlow=2 + +$z3 = Join-Path -AdditionalChildPath $x "" +Sink $z3 # $ hasTaintFlow=2 + +$z4 = Join-Path $x +Sink $z4 # $ hasTaintFlow=2 + +$z5 = Join-Path "" $x +Sink $z5 # $ hasTaintFlow=2 + +$z6 = Join-Path "" "" $x +Sink $z6 # $ hasTaintFlow=2 + +$z7 = [System.IO.Path]::GetFullPath($x) +Sink $z7 # $ hasTaintFlow=2 \ No newline at end of file diff --git a/powershell/ql/test/query-tests/security/cwe-022/test.ps1 b/powershell/ql/test/query-tests/security/cwe-022/test.ps1 new file mode 100644 index 000000000000..f0593424d182 --- /dev/null +++ b/powershell/ql/test/query-tests/security/cwe-022/test.ps1 @@ -0,0 +1,29 @@ +Add-Type -AssemblyName System.IO.Compression.FileSystem + +$zip = [System.IO.Compression.ZipFile]::OpenRead("MyPath\to\archive.zip") + +foreach ($entry in $zip.Entries) { + $targetPath = Join-Path $extractPath $entry.FullName + $fullTargetPath = [System.IO.Path]::GetFullPath($targetPath) + + [System.IO.Compression.ZipFileExtensions]::ExtractToFile($entry, $fullTargetPath) # BAD +} + +foreach ($entry in $zip.Entries) { + $targetPath = Join-Path $extractPath $entry.FullName + $fullTargetPath = [System.IO.Path]::GetFullPath($targetPath) + + $stream = [System.IO.File]::Open($fullTargetPath, 'Create') # BAD + $entry.Open().CopyTo($stream) + $stream.Close() +} + +foreach ($entry in $zip.Entries) { + $targetPath = Join-Path $extractPath $entry.FullName + $fullTargetPath = [System.IO.Path]::GetFullPath($targetPath) + + $extractRoot = [System.IO.Path]::GetFullPath($extractPath) + if ($fullTargetPath.StartsWith($extractRoot)) { + [System.IO.Compression.ZipFileExtensions]::ExtractToFile($entry, $fullTargetPath) # GOOD [FALSE POSITIVE] + } +} \ No newline at end of file diff --git a/powershell/ql/test/query-tests/security/cwe-022/zipslip.expected b/powershell/ql/test/query-tests/security/cwe-022/zipslip.expected new file mode 100644 index 000000000000..f87a61f40c07 --- /dev/null +++ b/powershell/ql/test/query-tests/security/cwe-022/zipslip.expected @@ -0,0 +1,64 @@ +edges +| file://:0:0:0:0 | [summary param] pos(0, {}) in microsoft.powershell.management!;Method[join-path] | file://:0:0:0:0 | [summary] to write: ReturnValue in microsoft.powershell.management!;Method[join-path] | provenance | MaD:36 | +| file://:0:0:0:0 | [summary param] pos(0, {}) in system.io.path!;Method[getfullpath] | file://:0:0:0:0 | [summary] to write: ReturnValue in system.io.path!;Method[getfullpath] | provenance | MaD:103 | +| test.ps1:6:5:6:15 | targetPath | test.ps1:7:53:7:63 | targetPath | provenance | | +| test.ps1:6:19:6:56 | Call to join-path | test.ps1:6:5:6:15 | targetPath | provenance | | +| test.ps1:6:42:6:56 | fullname | file://:0:0:0:0 | [summary param] pos(0, {}) in microsoft.powershell.management!;Method[join-path] | provenance | | +| test.ps1:6:42:6:56 | fullname | test.ps1:6:19:6:56 | Call to join-path | provenance | MaD:36 | +| test.ps1:7:5:7:19 | fullTargetPath | test.ps1:9:70:9:84 | fullTargetPath | provenance | | +| test.ps1:7:23:7:64 | Call to getfullpath | test.ps1:7:5:7:19 | fullTargetPath | provenance | | +| test.ps1:7:53:7:63 | targetPath | file://:0:0:0:0 | [summary param] pos(0, {}) in system.io.path!;Method[getfullpath] | provenance | | +| test.ps1:7:53:7:63 | targetPath | test.ps1:7:23:7:64 | Call to getfullpath | provenance | MaD:103 | +| test.ps1:13:5:13:15 | targetPath | test.ps1:14:53:14:63 | targetPath | provenance | | +| test.ps1:13:19:13:56 | Call to join-path | test.ps1:13:5:13:15 | targetPath | provenance | | +| test.ps1:13:42:13:56 | fullname | file://:0:0:0:0 | [summary param] pos(0, {}) in microsoft.powershell.management!;Method[join-path] | provenance | | +| test.ps1:13:42:13:56 | fullname | test.ps1:13:19:13:56 | Call to join-path | provenance | MaD:36 | +| test.ps1:14:5:14:19 | fullTargetPath | test.ps1:16:38:16:52 | fullTargetPath | provenance | | +| test.ps1:14:23:14:64 | Call to getfullpath | test.ps1:14:5:14:19 | fullTargetPath | provenance | | +| test.ps1:14:53:14:63 | targetPath | file://:0:0:0:0 | [summary param] pos(0, {}) in system.io.path!;Method[getfullpath] | provenance | | +| test.ps1:14:53:14:63 | targetPath | test.ps1:14:23:14:64 | Call to getfullpath | provenance | MaD:103 | +| test.ps1:22:5:22:15 | targetPath | test.ps1:23:53:23:63 | targetPath | provenance | | +| test.ps1:22:19:22:56 | Call to join-path | test.ps1:22:5:22:15 | targetPath | provenance | | +| test.ps1:22:42:22:56 | fullname | file://:0:0:0:0 | [summary param] pos(0, {}) in microsoft.powershell.management!;Method[join-path] | provenance | | +| test.ps1:22:42:22:56 | fullname | test.ps1:22:19:22:56 | Call to join-path | provenance | MaD:36 | +| test.ps1:23:5:23:19 | fullTargetPath | test.ps1:27:74:27:88 | fullTargetPath | provenance | | +| test.ps1:23:23:23:64 | Call to getfullpath | test.ps1:23:5:23:19 | fullTargetPath | provenance | | +| test.ps1:23:53:23:63 | targetPath | file://:0:0:0:0 | [summary param] pos(0, {}) in system.io.path!;Method[getfullpath] | provenance | | +| test.ps1:23:53:23:63 | targetPath | test.ps1:23:23:23:64 | Call to getfullpath | provenance | MaD:103 | +nodes +| file://:0:0:0:0 | [summary param] pos(0, {}) in microsoft.powershell.management!;Method[join-path] | semmle.label | [summary param] pos(0, {}) in microsoft.powershell.management!;Method[join-path] | +| file://:0:0:0:0 | [summary param] pos(0, {}) in system.io.path!;Method[getfullpath] | semmle.label | [summary param] pos(0, {}) in system.io.path!;Method[getfullpath] | +| file://:0:0:0:0 | [summary] to write: ReturnValue in microsoft.powershell.management!;Method[join-path] | semmle.label | [summary] to write: ReturnValue in microsoft.powershell.management!;Method[join-path] | +| file://:0:0:0:0 | [summary] to write: ReturnValue in system.io.path!;Method[getfullpath] | semmle.label | [summary] to write: ReturnValue in system.io.path!;Method[getfullpath] | +| test.ps1:6:5:6:15 | targetPath | semmle.label | targetPath | +| test.ps1:6:19:6:56 | Call to join-path | semmle.label | Call to join-path | +| test.ps1:6:42:6:56 | fullname | semmle.label | fullname | +| test.ps1:7:5:7:19 | fullTargetPath | semmle.label | fullTargetPath | +| test.ps1:7:23:7:64 | Call to getfullpath | semmle.label | Call to getfullpath | +| test.ps1:7:53:7:63 | targetPath | semmle.label | targetPath | +| test.ps1:9:70:9:84 | fullTargetPath | semmle.label | fullTargetPath | +| test.ps1:13:5:13:15 | targetPath | semmle.label | targetPath | +| test.ps1:13:19:13:56 | Call to join-path | semmle.label | Call to join-path | +| test.ps1:13:42:13:56 | fullname | semmle.label | fullname | +| test.ps1:14:5:14:19 | fullTargetPath | semmle.label | fullTargetPath | +| test.ps1:14:23:14:64 | Call to getfullpath | semmle.label | Call to getfullpath | +| test.ps1:14:53:14:63 | targetPath | semmle.label | targetPath | +| test.ps1:16:38:16:52 | fullTargetPath | semmle.label | fullTargetPath | +| test.ps1:22:5:22:15 | targetPath | semmle.label | targetPath | +| test.ps1:22:19:22:56 | Call to join-path | semmle.label | Call to join-path | +| test.ps1:22:42:22:56 | fullname | semmle.label | fullname | +| test.ps1:23:5:23:19 | fullTargetPath | semmle.label | fullTargetPath | +| test.ps1:23:23:23:64 | Call to getfullpath | semmle.label | Call to getfullpath | +| test.ps1:23:53:23:63 | targetPath | semmle.label | targetPath | +| test.ps1:27:74:27:88 | fullTargetPath | semmle.label | fullTargetPath | +subpaths +| test.ps1:6:42:6:56 | fullname | file://:0:0:0:0 | [summary param] pos(0, {}) in microsoft.powershell.management!;Method[join-path] | file://:0:0:0:0 | [summary] to write: ReturnValue in microsoft.powershell.management!;Method[join-path] | test.ps1:6:19:6:56 | Call to join-path | +| test.ps1:7:53:7:63 | targetPath | file://:0:0:0:0 | [summary param] pos(0, {}) in system.io.path!;Method[getfullpath] | file://:0:0:0:0 | [summary] to write: ReturnValue in system.io.path!;Method[getfullpath] | test.ps1:7:23:7:64 | Call to getfullpath | +| test.ps1:13:42:13:56 | fullname | file://:0:0:0:0 | [summary param] pos(0, {}) in microsoft.powershell.management!;Method[join-path] | file://:0:0:0:0 | [summary] to write: ReturnValue in microsoft.powershell.management!;Method[join-path] | test.ps1:13:19:13:56 | Call to join-path | +| test.ps1:14:53:14:63 | targetPath | file://:0:0:0:0 | [summary param] pos(0, {}) in system.io.path!;Method[getfullpath] | file://:0:0:0:0 | [summary] to write: ReturnValue in system.io.path!;Method[getfullpath] | test.ps1:14:23:14:64 | Call to getfullpath | +| test.ps1:22:42:22:56 | fullname | file://:0:0:0:0 | [summary param] pos(0, {}) in microsoft.powershell.management!;Method[join-path] | file://:0:0:0:0 | [summary] to write: ReturnValue in microsoft.powershell.management!;Method[join-path] | test.ps1:22:19:22:56 | Call to join-path | +| test.ps1:23:53:23:63 | targetPath | file://:0:0:0:0 | [summary param] pos(0, {}) in system.io.path!;Method[getfullpath] | file://:0:0:0:0 | [summary] to write: ReturnValue in system.io.path!;Method[getfullpath] | test.ps1:23:23:23:64 | Call to getfullpath | +#select +| test.ps1:6:42:6:56 | fullname | test.ps1:6:42:6:56 | fullname | test.ps1:9:70:9:84 | fullTargetPath | Unsanitized archive entry, which may contain '..', is used in a $@. | test.ps1:9:70:9:84 | fullTargetPath | file system operation | +| test.ps1:13:42:13:56 | fullname | test.ps1:13:42:13:56 | fullname | test.ps1:16:38:16:52 | fullTargetPath | Unsanitized archive entry, which may contain '..', is used in a $@. | test.ps1:16:38:16:52 | fullTargetPath | file system operation | +| test.ps1:22:42:22:56 | fullname | test.ps1:22:42:22:56 | fullname | test.ps1:27:74:27:88 | fullTargetPath | Unsanitized archive entry, which may contain '..', is used in a $@. | test.ps1:27:74:27:88 | fullTargetPath | file system operation | diff --git a/powershell/ql/test/query-tests/security/cwe-022/zipslip.qlref b/powershell/ql/test/query-tests/security/cwe-022/zipslip.qlref new file mode 100644 index 000000000000..1fb40016ea52 --- /dev/null +++ b/powershell/ql/test/query-tests/security/cwe-022/zipslip.qlref @@ -0,0 +1 @@ +queries/security/cwe-022/ZipSlip.ql \ No newline at end of file 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 9128aef9ba66..f5a0fe356c72 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 @@ -3,7 +3,7 @@ edges | 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:27:11:27:20 | userinput | test.ps1:28:38:28:67 | Get-Process -Name $UserInput | provenance | Sink:MaD:102 | +| test.ps1:27:11:27:20 | userinput | test.ps1:28:38:28:67 | Get-Process -Name $UserInput | provenance | Sink:MaD:106 | | 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:47:5:47:9 | code | provenance | | @@ -11,7 +11,7 @@ edges | 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 | Sink:MaD:101 | +| test.ps1:94:11:94:20 | userinput | test.ps1:98:33:98:62 | Get-Process -Name $UserInput | provenance | Sink:MaD:105 | | 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 | |