From 92c08e53b34cd67586d7fd714b1067248f8cc9b8 Mon Sep 17 00:00:00 2001 From: intrigus Date: Wed, 24 Jun 2020 23:58:58 +0200 Subject: [PATCH 01/11] [WIP] User-controlled read/write to user-controlled path. --- .../Security/CWE/CWE-706/PathsCommon.qll | 74 +++++ .../UserControlledArbitraryReadWrite.ql | 259 ++++++++++++++++++ 2 files changed, 333 insertions(+) create mode 100644 java/ql/src/experimental/Security/CWE/CWE-706/PathsCommon.qll create mode 100644 java/ql/src/experimental/Security/CWE/CWE-706/UserControlledArbitraryReadWrite.ql diff --git a/java/ql/src/experimental/Security/CWE/CWE-706/PathsCommon.qll b/java/ql/src/experimental/Security/CWE/CWE-706/PathsCommon.qll new file mode 100644 index 000000000000..362144d8c191 --- /dev/null +++ b/java/ql/src/experimental/Security/CWE/CWE-706/PathsCommon.qll @@ -0,0 +1,74 @@ +import java +import semmle.code.java.controlflow.Guards + +abstract class PathCreation extends Expr { + abstract Expr getInput(); +} + +class PathsGet extends PathCreation, MethodAccess { + PathsGet() { + exists(Method m | m = this.getMethod() | + m.getDeclaringType() instanceof TypePaths and + m.getName() = "get" + ) + } + + override Expr getInput() { result = this.getAnArgument() } +} + +class FileSystemGetPath extends PathCreation, MethodAccess { + FileSystemGetPath() { + exists(Method m | m = this.getMethod() | + m.getDeclaringType() instanceof TypeFileSystem and + m.getName() = "getPath" + ) + } + + override Expr getInput() { result = this.getAnArgument() } +} + +class FileCreation extends PathCreation, ClassInstanceExpr { + FileCreation() { this.getConstructedType() instanceof TypeFile } + + override Expr getInput() { + result = this.getAnArgument() and + // Relevant arguments include those that are not a `File`. + not result.getType() instanceof TypeFile + } +} + +class FileWriterCreation extends PathCreation, ClassInstanceExpr { + FileWriterCreation() { this.getConstructedType().getQualifiedName() = "java.io.FileWriter" } + + override Expr getInput() { + result = this.getAnArgument() and + // Relevant arguments are those of type `String`. + result.getType() instanceof TypeString + } +} + +predicate inWeakCheck(Expr e) { + // None of these are sufficient to guarantee that a string is safe. + exists(MethodAccess m, Method def | m.getQualifier() = e and m.getMethod() = def | + def.getName() = "startsWith" or + def.getName() = "endsWith" or + def.getName() = "isEmpty" or + def.getName() = "equals" + ) + or + // Checking against `null` has no bearing on path traversal. + exists(EqualityTest b | b.getAnOperand() = e | b.getAnOperand() instanceof NullLiteral) +} + +// Ignore cases where the variable has been checked somehow, +// but allow some particularly obviously bad cases. +predicate guarded(VarAccess e) { + exists(PathCreation p | e = p.getInput()) and + exists(ConditionBlock cb, Expr c | + cb.getCondition().getAChildExpr*() = c and + c = e.getVariable().getAnAccess() and + cb.controls(e.getBasicBlock(), true) and + // Disallow a few obviously bad checks. + not inWeakCheck(c) + ) +} diff --git a/java/ql/src/experimental/Security/CWE/CWE-706/UserControlledArbitraryReadWrite.ql b/java/ql/src/experimental/Security/CWE/CWE-706/UserControlledArbitraryReadWrite.ql new file mode 100644 index 000000000000..b2efbd549f02 --- /dev/null +++ b/java/ql/src/experimental/Security/CWE/CWE-706/UserControlledArbitraryReadWrite.ql @@ -0,0 +1,259 @@ +/** + * @name User-controlled read/write on user-controlled path expression + * @description Reading/writing from/to paths influenced by users can allow an attacker to read or write attacker-controlled content to an arbitrary resources . + * @kind path-problem + * @problem.severity error + * @precision high + * @id java/tainted-file-read-write + * @tags security + * external/cwe/cwe-706 + */ + +import java +import semmle.code.java.dataflow.FlowSources +import semmle.code.java.dataflow.TaintTracking2 +import semmle.code.java.dataflow.TaintTracking +import semmle.code.java.security.XSS +import DataFlow2::PathGraph +import PathsCommon + +/** The class `java.io.FileInputStream`. */ +class TypeFileInputStream extends Class { + TypeFileInputStream() { this.hasQualifiedName("java.io", "FileInputStream") } +} + +/** The class `java.nio.file.Files`. */ +class TypeFiles extends Class { + TypeFiles() { this.hasQualifiedName("java.nio.file", "Files") } +} + +/** The class `org.json.JSONObject`. */ +class TypeJsonObject extends Class { + TypeJsonObject() { this.hasQualifiedName("org.json", "JSONObject") } +} + +/** The class `org.json.JSONArray`. */ +class TypeJsonArray extends Class { + TypeJsonArray() { this.hasQualifiedName("org.json", "JSONArray") } +} + +/** The class `ai.susi.server.ServiceResponse`. */ +class TypeServiceResponse extends Class { + TypeServiceResponse() { this.hasQualifiedName("ai.susi.server", "ServiceResponse") } +} + +class ServiceResponseSink extends DataFlow::ExprNode { + ServiceResponseSink() { + exists(ConstructorCall call | + call.getConstructedType() instanceof TypeServiceResponse and + this.getExpr() = call.getAnArgument() + ) + or + exists(MethodAccess call | + call.getType() instanceof TypeServiceResponse and + this.getExpr() = call.getAnArgument() + ) + } +} + +predicate deletesFile(DataFlow::ExprNode node) { + exists(MethodAccess call | + call.getReceiverType() instanceof TypeFile and + call.getMethod().getName().matches("delete%") and + node.getExpr() = call.getQualifier() + ) +} + +predicate deletesPath(DataFlow::ExprNode node) { + exists(MethodAccess call | + call.getReceiverType() instanceof TypeFiles and + call.getMethod().getName().matches("delete%") and + node.getExpr() = call.getArgument(0) + ) +} + +predicate renamesFile(DataFlow::ExprNode node) { + exists(MethodAccess call | + call.getReceiverType() instanceof TypeFile and + call.getMethod().getName().matches("renameTo%") and + ( + node.getExpr() = call.getQualifier() + or + node.getExpr() = call.getArgument(0) + ) + ) +} + +predicate renamesPath(DataFlow::ExprNode node) { + exists(MethodAccess call | + call.getReceiverType() instanceof TypeFiles and + call.getMethod().getName().matches("move%") and + ( + node.getExpr() = call.getArgument(0) + or + node.getExpr() = call.getArgument(1) + ) + ) +} + +class SensitiveFileOperationSink extends DataFlow::ExprNode { + SensitiveFileOperationSink() { + deletesFile(this) + or + deletesPath(this) + or + renamesFile(this) + or + renamesPath(this) + } +} + +predicate usedInPathCreation(DataFlow::Node node1, DataFlow::Node node2) { + exists(Expr e | e = node1.asExpr() | + e = node2.asExpr().(PathCreation).getInput() and not guarded(e) + ) +} + +predicate putsValueIntoJsonObject(DataFlow::Node node1, DataFlow::Node node2) { + exists(MethodAccess call, string methodName | + call.getReceiverType() instanceof TypeJsonObject and + call.getMethod().getName() = methodName and + (methodName = "put" or methodName = "putOnce" or methodName = "putOpt") and + call.getQualifier() = node2.asExpr() and + call.getArgument(1) = node1.asExpr() + ) +} + +predicate putsValueIntoJsonArray(DataFlow::Node node1, DataFlow::Node node2) { + exists(MethodAccess call | + call.getReceiverType() instanceof TypeJsonArray and + call.getMethod().getName() = "put" and + call.getQualifier() = node2.asExpr() and + ( + call.getArgument(1) = node1.asExpr() and call.getNumArgument() = 2 + or + call.getArgument(0) = node1.asExpr() and call.getNumArgument() = 1 + ) + ) +} + +predicate isFileToPath(DataFlow::Node node1, DataFlow::Node node2) { + exists(MethodAccess call | + call.getReceiverType() instanceof TypeFile and + call.getMethod().hasName("toPath") and + call = node2.asExpr() and + call.getQualifier() = node1.asExpr() + ) +} + +predicate isPathToFile(DataFlow::Node node1, DataFlow::Node node2) { + exists(MethodAccess call | + call.getReceiverType() instanceof TypePath and + call.getMethod().hasName("toFile") and + call = node2.asExpr() and + call.getQualifier() = node1.asExpr() + ) +} + +predicate readsAllBytesFromPath(DataFlow::Node node1, DataFlow::Node node2) { + exists(MethodAccess call | + call.getReceiverType() instanceof TypeFiles and + (call.getMethod().hasName("readAllBytes") or call.getMethod().hasName("readAllLines")) and + call = node2.asExpr() and + call.getArgument(0) = node1.asExpr() + ) +} + +predicate inputStreamReadsFromFile(DataFlow::Node node1, DataFlow::Node node2) { + exists(ConstructorCall call | + call.getConstructedType() instanceof TypeFileInputStream and + call = node2.asExpr() and + call.getAnArgument() = node1.asExpr() + ) +} + +predicate taintedNewFile(DataFlow::Node node1, DataFlow::Node node2) { + exists(ConstructorCall call | + call.getConstructedType() instanceof TypeFile and + call = node2.asExpr() and + call.getAnArgument() = node1.asExpr() + ) +} + +class ContainsDotDotSanitizer extends DataFlow::BarrierGuard { + ContainsDotDotSanitizer() { + this.(MethodAccess).getMethod().hasName("contains") and + this.(MethodAccess).getAnArgument().(StringLiteral).getValue() = ".." + } + + override predicate checks(Expr e, boolean branch) { + e = this.(MethodAccess).getQualifier() and branch = false + } +} + +class TaintedPathConfig extends TaintTracking::Configuration { + TaintedPathConfig() { this = "TaintedPathConfig" } + + override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource } + + override predicate isSink(DataFlow::Node sink) { + exists(Expr e | e = sink.asExpr() | e = any(PathCreation p).getInput() and not guarded(e)) + } + + override predicate isSanitizer(DataFlow::Node node) { + exists(Type t | t = node.getType() | t instanceof BoxedType or t instanceof PrimitiveType) + } + + override predicate isSanitizerGuard(DataFlow::BarrierGuard guard) { + guard instanceof ContainsDotDotSanitizer + } +} + +class XSSConfig extends TaintTracking2::Configuration { + XSSConfig() { this = "XSSConfig" } + + override predicate isSource(DataFlow::Node source) { + any() //source.asExpr().getType() instanceof TypePath //any()//source instanceof RemoteFlowSource + } //source.asExpr().getFile().getBaseName().matches("GetSkillJsonService.java")}//any()}//source instanceof RemoteFlowSource } + + override predicate isSink(DataFlow::Node sink) { + sink instanceof ServiceResponseSink or + sink instanceof XssSink or + sink instanceof SensitiveFileOperationSink + } + + override predicate isSanitizer(DataFlow::Node node) { + node.getType() instanceof NumericType or node.getType() instanceof BooleanType + } + + override predicate isAdditionalTaintStep(DataFlow::Node node1, DataFlow::Node node2) { + usedInPathCreation(node1, node2) + or + inputStreamReadsFromFile(node1, node2) + or + isFileToPath(node1, node2) + or + isPathToFile(node1, node2) + or + readsAllBytesFromPath(node1, node2) + or + putsValueIntoJsonObject(node1, node2) + or + putsValueIntoJsonArray(node1, node2) + or + taintedNewFile(node1, node2) + } +} + +from + DataFlow::PathNode remoteSource, DataFlow::PathNode taintedFile, DataFlow2::PathNode taintedFile2, + DataFlow2::PathNode infoLeak, XSSConfig conf, TaintedPathConfig taintedPathConf, PathCreation p +where + taintedPathConf.hasFlowPath(remoteSource, taintedFile) and + taintedFile.getNode().asExpr() = p.getInput() and + taintedFile.getNode().asExpr() = taintedFile2.getNode().asExpr() and + //TaintTracking::localExprTaint(p, taintedFile2.getNode().asExpr()) and + conf.hasFlowPath(taintedFile2, infoLeak) +select infoLeak.getNode(), taintedFile2, infoLeak, + "Sensitive file operation or information leak due to $@.", taintedFile2.getNode(), + "user-provided value" From 2d50c9f93452a97bc593672f7e88c86ca18b95f4 Mon Sep 17 00:00:00 2001 From: intrigus Date: Thu, 25 Jun 2020 17:37:08 +0200 Subject: [PATCH 02/11] Simplify Query --- .../UserControlledArbitraryReadWrite.ql | 76 ++++++++++++------- 1 file changed, 50 insertions(+), 26 deletions(-) diff --git a/java/ql/src/experimental/Security/CWE/CWE-706/UserControlledArbitraryReadWrite.ql b/java/ql/src/experimental/Security/CWE/CWE-706/UserControlledArbitraryReadWrite.ql index b2efbd549f02..c5e231aa59dc 100644 --- a/java/ql/src/experimental/Security/CWE/CWE-706/UserControlledArbitraryReadWrite.ql +++ b/java/ql/src/experimental/Security/CWE/CWE-706/UserControlledArbitraryReadWrite.ql @@ -197,7 +197,8 @@ class TaintedPathConfig extends TaintTracking::Configuration { override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource } override predicate isSink(DataFlow::Node sink) { - exists(Expr e | e = sink.asExpr() | e = any(PathCreation p).getInput() and not guarded(e)) + exists(TaintedPathSink s | s.getTaintedFile() = sink.asExpr()) + // sink instanceof TaintedPathSink } override predicate isSanitizer(DataFlow::Node node) { @@ -207,19 +208,37 @@ class TaintedPathConfig extends TaintTracking::Configuration { override predicate isSanitizerGuard(DataFlow::BarrierGuard guard) { guard instanceof ContainsDotDotSanitizer } + + override predicate isAdditionalTaintStep(DataFlow::Node node1, DataFlow::Node node2) { + usedInPathCreation(node1, node2) + } +} + +private class TaintedPathSink extends DataFlow::Node { + Expr path; + + TaintedPathSink() { + exists(Expr e, PathCreation p | e = asExpr() | e = p.getInput() and not guarded(e) and path = p) + } + + Expr getTaintedFile() { result = path } } -class XSSConfig extends TaintTracking2::Configuration { - XSSConfig() { this = "XSSConfig" } +class InformationLeakConfig extends TaintTracking2::Configuration { + InformationLeakConfig() { this = "InformationLeakConfig" } override predicate isSource(DataFlow::Node source) { - any() //source.asExpr().getType() instanceof TypePath //any()//source instanceof RemoteFlowSource + exists(TaintedPathSink s | s.getTaintedFile() = source.asExpr()) + //source instanceof TaintedPathSink + //any() //source.asExpr().getType() instanceof TypePath //any()//source instanceof RemoteFlowSource } //source.asExpr().getFile().getBaseName().matches("GetSkillJsonService.java")}//any()}//source instanceof RemoteFlowSource } override predicate isSink(DataFlow::Node sink) { - sink instanceof ServiceResponseSink or - sink instanceof XssSink or - sink instanceof SensitiveFileOperationSink + sink instanceof RemoteFlowSink + or + //sink instanceof ServiceResponseSink or + sink instanceof XssSink //or + //sink instanceof SensitiveFileOperationSink } override predicate isSanitizer(DataFlow::Node node) { @@ -227,33 +246,38 @@ class XSSConfig extends TaintTracking2::Configuration { } override predicate isAdditionalTaintStep(DataFlow::Node node1, DataFlow::Node node2) { - usedInPathCreation(node1, node2) - or + // none() + // usedInPathCreation(node1, node2) + //or inputStreamReadsFromFile(node1, node2) or - isFileToPath(node1, node2) - or - isPathToFile(node1, node2) - or - readsAllBytesFromPath(node1, node2) - or - putsValueIntoJsonObject(node1, node2) - or - putsValueIntoJsonArray(node1, node2) - or + /* + * or + * isFileToPath(node1, node2) + * or + * isPathToFile(node1, node2) + * or + * readsAllBytesFromPath(node1, node2) + * or + * putsValueIntoJsonObject(node1, node2) + * or + * putsValueIntoJsonArray(node1, node2) + */ + taintedNewFile(node1, node2) + /* */ } } from DataFlow::PathNode remoteSource, DataFlow::PathNode taintedFile, DataFlow2::PathNode taintedFile2, - DataFlow2::PathNode infoLeak, XSSConfig conf, TaintedPathConfig taintedPathConf, PathCreation p + DataFlow2::PathNode infoLeak, InformationLeakConfig infoLeakConf, + TaintedPathConfig taintedPathConf //, PathCreation p where taintedPathConf.hasFlowPath(remoteSource, taintedFile) and - taintedFile.getNode().asExpr() = p.getInput() and - taintedFile.getNode().asExpr() = taintedFile2.getNode().asExpr() and + taintedFile.getNode() = taintedFile2.getNode() and + //p = taintedFile2.getNode().asExpr() and //TaintTracking::localExprTaint(p, taintedFile2.getNode().asExpr()) and - conf.hasFlowPath(taintedFile2, infoLeak) -select infoLeak.getNode(), taintedFile2, infoLeak, - "Sensitive file operation or information leak due to $@.", taintedFile2.getNode(), - "user-provided value" + infoLeakConf.hasFlowPath(taintedFile2, infoLeak) +select infoLeak.getNode(), taintedFile2, infoLeak, "Information leak due to $@.", + taintedFile2.getNode(), "user-provided value" From ec8d7d98a90c9a1066c3590a405701bfdd15a097 Mon Sep 17 00:00:00 2001 From: intrigus Date: Thu, 25 Jun 2020 17:37:59 +0200 Subject: [PATCH 03/11] Add RemoteFlowSink class --- .../src/semmle/code/java/dataflow/RemoteFlowSinks.qll | 9 +++++++++ java/ql/src/semmle/code/java/security/XSS.qll | 11 +++++++---- 2 files changed, 16 insertions(+), 4 deletions(-) create mode 100644 java/ql/src/semmle/code/java/dataflow/RemoteFlowSinks.qll diff --git a/java/ql/src/semmle/code/java/dataflow/RemoteFlowSinks.qll b/java/ql/src/semmle/code/java/dataflow/RemoteFlowSinks.qll new file mode 100644 index 000000000000..07d57068bbcb --- /dev/null +++ b/java/ql/src/semmle/code/java/dataflow/RemoteFlowSinks.qll @@ -0,0 +1,9 @@ +/** + * Provides classes representing data flow sinks for remote user output. + */ + +import java +private import semmle.code.java.security.XSS + +/** A data flow sink of remote user output. */ +abstract class RemoteFlowSink extends DataFlow::Node { } diff --git a/java/ql/src/semmle/code/java/security/XSS.qll b/java/ql/src/semmle/code/java/security/XSS.qll index 1b75b9ed649c..1d8e22e9ab00 100644 --- a/java/ql/src/semmle/code/java/security/XSS.qll +++ b/java/ql/src/semmle/code/java/security/XSS.qll @@ -2,12 +2,15 @@ import java import semmle.code.java.frameworks.Servlets import semmle.code.java.frameworks.android.WebView import semmle.code.java.dataflow.TaintTracking +import semmle.code.java.dataflow.RemoteFlowSinks -/* - * Definitions for XSS sinks +/** + * A data flow sink for cross-site scripting (XSS) vulnerabilities. + * + * Any XSS sink is also a remote flow sink, so this class contributes + * to the abstract class `RemoteFlowSink`. */ - -class XssSink extends DataFlow::ExprNode { +class XssSink extends DataFlow::ExprNode, RemoteFlowSink { XssSink() { exists(HttpServletResponseSendErrorMethod m, MethodAccess ma | ma.getMethod() = m and From 842084b110c2d1a93419c0a529546d46cddf3893 Mon Sep 17 00:00:00 2001 From: intrigus Date: Thu, 25 Jun 2020 17:38:47 +0200 Subject: [PATCH 04/11] Add documentation --- .../ql/src/experimental/Security/CWE/CWE-706/PathsCommon.qll | 5 +++++ .../Security/CWE/CWE-706/UserControlledArbitraryReadWrite.ql | 1 + 2 files changed, 6 insertions(+) diff --git a/java/ql/src/experimental/Security/CWE/CWE-706/PathsCommon.qll b/java/ql/src/experimental/Security/CWE/CWE-706/PathsCommon.qll index 362144d8c191..ddcc2c64e975 100644 --- a/java/ql/src/experimental/Security/CWE/CWE-706/PathsCommon.qll +++ b/java/ql/src/experimental/Security/CWE/CWE-706/PathsCommon.qll @@ -2,9 +2,11 @@ import java import semmle.code.java.controlflow.Guards abstract class PathCreation extends Expr { + /** Gets an input that is used in the creation of this path. */ abstract Expr getInput(); } +/** Models the `java.nio.file.Paths.get` method. */ class PathsGet extends PathCreation, MethodAccess { PathsGet() { exists(Method m | m = this.getMethod() | @@ -16,6 +18,7 @@ class PathsGet extends PathCreation, MethodAccess { override Expr getInput() { result = this.getAnArgument() } } +/** Models the `java.nio.file.FileSystem.getPath` method. */ class FileSystemGetPath extends PathCreation, MethodAccess { FileSystemGetPath() { exists(Method m | m = this.getMethod() | @@ -27,6 +30,7 @@ class FileSystemGetPath extends PathCreation, MethodAccess { override Expr getInput() { result = this.getAnArgument() } } +/** Models the `new java.io.File(...)` constructor. */ class FileCreation extends PathCreation, ClassInstanceExpr { FileCreation() { this.getConstructedType() instanceof TypeFile } @@ -37,6 +41,7 @@ class FileCreation extends PathCreation, ClassInstanceExpr { } } +/** Models the `new java.io.FileWriter(...)` constructor. */ class FileWriterCreation extends PathCreation, ClassInstanceExpr { FileWriterCreation() { this.getConstructedType().getQualifiedName() = "java.io.FileWriter" } diff --git a/java/ql/src/experimental/Security/CWE/CWE-706/UserControlledArbitraryReadWrite.ql b/java/ql/src/experimental/Security/CWE/CWE-706/UserControlledArbitraryReadWrite.ql index c5e231aa59dc..6d43ccb2eaa1 100644 --- a/java/ql/src/experimental/Security/CWE/CWE-706/UserControlledArbitraryReadWrite.ql +++ b/java/ql/src/experimental/Security/CWE/CWE-706/UserControlledArbitraryReadWrite.ql @@ -108,6 +108,7 @@ class SensitiveFileOperationSink extends DataFlow::ExprNode { } } +/** Holds if `node1` is used in the creation of `node2` and not guarded. */ predicate usedInPathCreation(DataFlow::Node node1, DataFlow::Node node2) { exists(Expr e | e = node1.asExpr() | e = node2.asExpr().(PathCreation).getInput() and not guarded(e) From 411be40001288eb8441e22ea5f237a1b0c0c73fb Mon Sep 17 00:00:00 2001 From: intrigus Date: Thu, 25 Jun 2020 17:39:08 +0200 Subject: [PATCH 05/11] Use defined Types, add java.nio.file.Files --- java/ql/src/Security/CWE/CWE-022/ZipSlip.ql | 2 +- java/ql/src/semmle/code/java/JDK.qll | 5 +++++ java/ql/src/semmle/code/java/security/FileReadWrite.qll | 2 +- java/ql/src/semmle/code/java/security/FileWritable.qll | 6 +++--- 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/java/ql/src/Security/CWE/CWE-022/ZipSlip.ql b/java/ql/src/Security/CWE/CWE-022/ZipSlip.ql index 7d74f8b79ac4..b1011809143e 100644 --- a/java/ql/src/Security/CWE/CWE-022/ZipSlip.ql +++ b/java/ql/src/Security/CWE/CWE-022/ZipSlip.ql @@ -49,7 +49,7 @@ class WrittenFileName extends Expr { or // Methods that write to their n'th argument exists(MethodAccess call, int n | this = call.getArgument(n) | - call.getMethod().getDeclaringType().hasQualifiedName("java.nio.file", "Files") and + call.getMethod().getDeclaringType() instanceof TypeFiles and ( call.getMethod().getName().regexpMatch("new.*Reader|newOutputStream|create.*") and n = 0 or diff --git a/java/ql/src/semmle/code/java/JDK.qll b/java/ql/src/semmle/code/java/JDK.qll index d9a1a15e5d3d..6e4dce7bc29b 100644 --- a/java/ql/src/semmle/code/java/JDK.qll +++ b/java/ql/src/semmle/code/java/JDK.qll @@ -165,6 +165,11 @@ class TypeFileSystem extends Class { TypeFileSystem() { this.hasQualifiedName("java.nio.file", "FileSystem") } } +/** The class `java.nio.file.Files`. */ +class TypeFiles extends Class { + TypeFiles() { this.hasQualifiedName("java.nio.file", "Files") } +} + /** The class `java.io.File`. */ class TypeFile extends Class { TypeFile() { this.hasQualifiedName("java.io", "File") } diff --git a/java/ql/src/semmle/code/java/security/FileReadWrite.qll b/java/ql/src/semmle/code/java/security/FileReadWrite.qll index 68cd987532c0..769917ec509f 100644 --- a/java/ql/src/semmle/code/java/security/FileReadWrite.qll +++ b/java/ql/src/semmle/code/java/security/FileReadWrite.qll @@ -20,7 +20,7 @@ private predicate fileRead(VarAccess fileAccess, Expr fileReadingExpr) { ( // Identify all method calls on the `Files` class that imply that we are reading the file // represented by the first argument. - filesMethod.getDeclaringType().hasQualifiedName("java.nio.file", "Files") and + filesMethod.getDeclaringType() instanceof TypeFiles and fileAccess = ma.getArgument(0) and ( filesMethod.hasName("readAllBytes") or diff --git a/java/ql/src/semmle/code/java/security/FileWritable.qll b/java/ql/src/semmle/code/java/security/FileWritable.qll index fbd359517e7c..95de94944151 100644 --- a/java/ql/src/semmle/code/java/security/FileWritable.qll +++ b/java/ql/src/semmle/code/java/security/FileWritable.qll @@ -60,7 +60,7 @@ private EnumConstant getAContainedEnumConstant(Expr enumSetRef) { * Gets a `VarAccess` to a `File` that is converted to a `Path` by `pathExpr`. */ private VarAccess getFileForPathConversion(Expr pathExpr) { - pathExpr.getType().(RefType).hasQualifiedName("java.nio.file", "Path") and + pathExpr.getType() instanceof TypePath and ( // Look for conversion from `File` to `Path` using `file.getPath()`. exists(MethodAccess fileToPath | @@ -74,7 +74,7 @@ private VarAccess getFileForPathConversion(Expr pathExpr) { exists(MethodAccess pathsGet, MethodAccess fileGetPath | pathsGet = pathExpr and pathsGet.getMethod().hasName("get") and - pathsGet.getMethod().getDeclaringType().hasQualifiedName("java.nio.file", "Paths") and + pathsGet.getMethod().getDeclaringType() instanceof TypePaths and fileGetPath = pathsGet.getArgument(0) and result = fileGetPath.getQualifier() | @@ -105,7 +105,7 @@ private predicate fileSetWorldWritable(VarAccess fileAccess, Expr setWorldWritab exists(MethodAccess setPosixPerms | setPosixPerms = setWorldWritable and setPosixPerms.getMethod().hasName("setPosixFilePermissions") and - setPosixPerms.getMethod().getDeclaringType().hasQualifiedName("java.nio.file", "Files") and + setPosixPerms.getMethod().getDeclaringType() instanceof TypeFiles and ( fileAccess = setPosixPerms.getArgument(0) or From 6619ecbc68f4dcd828e7afce1ad6b8fdff51b472 Mon Sep 17 00:00:00 2001 From: intrigus Date: Thu, 25 Jun 2020 23:25:54 +0200 Subject: [PATCH 06/11] Seperate read and write query --- .../Security/CWE/CWE-706/PathsCommon.qll | 70 ++++++ .../CWE-706/UserControlledArbitraryRead.ql | 221 ++++++++++++++++++ .../UserControlledArbitraryReadWrite.ql | 117 +++------- .../CWE-706/UserControlledArbitraryWrite.ql | 221 ++++++++++++++++++ 4 files changed, 539 insertions(+), 90 deletions(-) create mode 100644 java/ql/src/experimental/Security/CWE/CWE-706/UserControlledArbitraryRead.ql create mode 100644 java/ql/src/experimental/Security/CWE/CWE-706/UserControlledArbitraryWrite.ql diff --git a/java/ql/src/experimental/Security/CWE/CWE-706/PathsCommon.qll b/java/ql/src/experimental/Security/CWE/CWE-706/PathsCommon.qll index ddcc2c64e975..8dfd5bc096d7 100644 --- a/java/ql/src/experimental/Security/CWE/CWE-706/PathsCommon.qll +++ b/java/ql/src/experimental/Security/CWE/CWE-706/PathsCommon.qll @@ -1,5 +1,7 @@ import java import semmle.code.java.controlflow.Guards +import semmle.code.java.dataflow.DataFlow +import semmle.code.java.dataflow.TaintTracking abstract class PathCreation extends Expr { /** Gets an input that is used in the creation of this path. */ @@ -77,3 +79,71 @@ predicate guarded(VarAccess e) { not inWeakCheck(c) ) } + +/** The class `java.io.FileInputStream`. */ +class TypeFileInputStream extends Class { + TypeFileInputStream() { this.hasQualifiedName("java.io", "FileInputStream") } +} + +/** Models additional taint steps like `file.toPath()`, `path.toFile()`, `new FileInputStream(..)`, `Files.readAll{Bytes|Lines}(...)`, and `new File(...)`. */ +class PathAdditionalTaintStep extends TaintTracking::AdditionalTaintStep { + override predicate step(DataFlow::Node node1, DataFlow::Node node2) { + inputStreamReadsFromFile(node1, node2) + or + isFileToPath(node1, node2) + or + isPathToFile(node1, node2) + or + readsAllFromPath(node1, node2) + or + taintedNewFile(node1, node2) + } +} + +/** Holds if `node1` is converted to `node2` via a call to `node1.toPath()`. */ +private predicate isFileToPath(DataFlow::Node node1, DataFlow::Node node2) { + exists(MethodAccess call | + call.getReceiverType() instanceof TypeFile and + call.getMethod().hasName("toPath") and + call = node2.asExpr() and + call.getQualifier() = node1.asExpr() + ) +} + +/** Holds if `node1` is converted to `node2` via a call to `node1.toFile()`. */ +private predicate isPathToFile(DataFlow::Node node1, DataFlow::Node node2) { + exists(MethodAccess call | + call.getReceiverType() instanceof TypePath and + call.getMethod().hasName("toFile") and + call = node2.asExpr() and + call.getQualifier() = node1.asExpr() + ) +} + +/** Holds if `node1` is read by `node2` via a call to `Files.readAllBytes(node1)` or `Files.readAllLines(node1)`. */ +private predicate readsAllFromPath(DataFlow::Node node1, DataFlow::Node node2) { + exists(MethodAccess call | + call.getReceiverType() instanceof TypeFiles and + call.getMethod().hasName(["readAllBytes", "readAllLines"]) and + call = node2.asExpr() and + call.getArgument(0) = node1.asExpr() + ) +} + +/** Holds if `node1` is passed to `node2` via a call to `new FileInputStream(node1)`. */ +private predicate inputStreamReadsFromFile(DataFlow::Node node1, DataFlow::Node node2) { + exists(ConstructorCall call | + call.getConstructedType() instanceof TypeFileInputStream and + call = node2.asExpr() and + call.getAnArgument() = node1.asExpr() + ) +} + +/** Holds if `node1` is passed to `node2` via a call to `new File(node1)`. */ +private predicate taintedNewFile(DataFlow::Node node1, DataFlow::Node node2) { + exists(ConstructorCall call | + call.getConstructedType() instanceof TypeFile and + call = node2.asExpr() and + call.getAnArgument() = node1.asExpr() + ) +} diff --git a/java/ql/src/experimental/Security/CWE/CWE-706/UserControlledArbitraryRead.ql b/java/ql/src/experimental/Security/CWE/CWE-706/UserControlledArbitraryRead.ql new file mode 100644 index 000000000000..abdb20da0477 --- /dev/null +++ b/java/ql/src/experimental/Security/CWE/CWE-706/UserControlledArbitraryRead.ql @@ -0,0 +1,221 @@ +/** + * @name Disclosure of user-controlled path expression + * @description Disclosing content from paths influenced by users can allow an attacker to read arbitrary resources. + * @kind path-problem + * @problem.severity error + * @precision medium + * @id java/tainted-file-read + * @tags security + * external/cwe/cwe-706 + */ + +import java +import semmle.code.java.dataflow.FlowSources +import semmle.code.java.dataflow.TaintTracking2 +import semmle.code.java.security.XSS +import DataFlow2::PathGraph +import PathsCommon + +/** The class `org.json.JSONObject`. */ +class TypeJsonObject extends Class { + TypeJsonObject() { this.hasQualifiedName("org.json", "JSONObject") } +} + +/** The class `org.json.JSONArray`. */ +class TypeJsonArray extends Class { + TypeJsonArray() { this.hasQualifiedName("org.json", "JSONArray") } +} + +/** The class `ai.susi.server.ServiceResponse`. */ +class TypeServiceResponse extends Class { + TypeServiceResponse() { this.hasQualifiedName("ai.susi.server", "ServiceResponse") } +} + +class ServiceResponseSink extends DataFlow::ExprNode { + ServiceResponseSink() { + exists(ConstructorCall call | + call.getConstructedType() instanceof TypeServiceResponse and + this.getExpr() = call.getAnArgument() + ) + or + exists(MethodAccess call | + call.getType() instanceof TypeServiceResponse and + this.getExpr() = call.getAnArgument() + ) + } +} + +predicate deletesFile(DataFlow::ExprNode node) { + exists(MethodAccess call | + call.getReceiverType() instanceof TypeFile and + call.getMethod().getName().matches("delete%") and + node.getExpr() = call.getQualifier() + ) +} + +predicate deletesPath(DataFlow::ExprNode node) { + exists(MethodAccess call | + call.getReceiverType() instanceof TypeFiles and + call.getMethod().getName().matches("delete%") and + node.getExpr() = call.getArgument(0) + ) +} + +predicate renamesFile(DataFlow::ExprNode node) { + exists(MethodAccess call | + call.getReceiverType() instanceof TypeFile and + call.getMethod().getName().matches("renameTo%") and + ( + node.getExpr() = call.getQualifier() + or + node.getExpr() = call.getArgument(0) + ) + ) +} + +predicate renamesPath(DataFlow::ExprNode node) { + exists(MethodAccess call | + call.getReceiverType() instanceof TypeFiles and + call.getMethod().getName().matches("move%") and + ( + node.getExpr() = call.getArgument(0) + or + node.getExpr() = call.getArgument(1) + ) + ) +} + +class SensitiveFileOperationSink extends DataFlow::ExprNode { + SensitiveFileOperationSink() { + deletesFile(this) + or + deletesPath(this) + or + renamesFile(this) + or + renamesPath(this) + } +} + +/** Holds if `node1` is used in the creation of `node2` and not guarded. */ +predicate usedInPathCreation(DataFlow::Node node1, DataFlow::Node node2) { + exists(Expr e | e = node1.asExpr() | + e = node2.asExpr().(PathCreation).getInput() and not guarded(e) + ) +} + +predicate putsValueIntoJsonObject(DataFlow::Node node1, DataFlow::Node node2) { + exists(MethodAccess call | + call.getReceiverType() instanceof TypeJsonObject and + call.getMethod().getName() = ["put", "putOnce", "putOpt"] and + call.getQualifier() = node2.asExpr() and + call.getArgument(1) = node1.asExpr() + ) +} + +predicate putsValueIntoJsonArray(DataFlow::Node node1, DataFlow::Node node2) { + exists(MethodAccess call | + call.getReceiverType() instanceof TypeJsonArray and + call.getMethod().getName() = "put" and + call.getQualifier() = node2.asExpr() and + ( + call.getArgument(1) = node1.asExpr() and call.getNumArgument() = 2 + or + call.getArgument(0) = node1.asExpr() and call.getNumArgument() = 1 + ) + ) +} + +class ContainsDotDotSanitizer extends DataFlow::BarrierGuard { + ContainsDotDotSanitizer() { + this.(MethodAccess).getMethod().hasName("contains") and + this.(MethodAccess).getAnArgument().(StringLiteral).getValue() = ".." + } + + override predicate checks(Expr e, boolean branch) { + e = this.(MethodAccess).getQualifier() and branch = false + } +} + +class TaintedPathConfig extends TaintTracking::Configuration { + TaintedPathConfig() { this = "TaintedPathConfig" } + + override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource } + + override predicate isSink(DataFlow::Node sink) { sink instanceof TaintedPathSink } + + override predicate isSanitizer(DataFlow::Node node) { + exists(Type t | t = node.getType() | t instanceof BoxedType or t instanceof PrimitiveType) + } + + override predicate isSanitizerGuard(DataFlow::BarrierGuard guard) { + guard instanceof ContainsDotDotSanitizer + // TODO add guards from zipslip.ql + } + + override predicate isAdditionalTaintStep(DataFlow::Node node1, DataFlow::Node node2) { + usedInPathCreation(node1, node2) + } +} + +private class TaintedPathSink extends DataFlow::Node { + Expr path; + Expr taintedInput; + + TaintedPathSink() { + exists(Expr e, PathCreation p | e = asExpr() | + e = p.getInput() and not guarded(e) and path = p and taintedInput = e + ) + } + + Expr getTaintedFile() { result = path } + + Expr getTaintedFileInput() { result = taintedInput } +} + +class InformationLeakConfig extends TaintTracking2::Configuration { + InformationLeakConfig() { this = "InformationLeakConfig" } + + override predicate isSource(DataFlow::Node source) { + source instanceof TaintedPathSink + //exists(TaintedPathSink s | s.getTaintedFile() = source.asExpr()) + //source instanceof TaintedPathSink + //any() //source.asExpr().getType() instanceof TypePath //any()//source instanceof RemoteFlowSource + } //source.asExpr().getFile().getBaseName().matches("GetSkillJsonService.java")}//any()}//source instanceof RemoteFlowSource } + + override predicate isSink(DataFlow::Node sink) { + sink instanceof RemoteFlowSink + or + //sink instanceof ServiceResponseSink or + sink instanceof XssSink //or + // sink instanceof SensitiveFileOperationSink + } + + override predicate isSanitizer(DataFlow::Node node) { + node.getType() instanceof NumericType or node.getType() instanceof BooleanType + } + + override predicate isAdditionalTaintStep(DataFlow::Node node1, DataFlow::Node node2) { + usedInPathCreation(node1, node2) + /* + * or + * putsValueIntoJsonObject(node1, node2) + * or + * putsValueIntoJsonArray(node1, node2) + */ + + } +} + +from + DataFlow::PathNode remoteSource, DataFlow::PathNode taintedFile, DataFlow2::PathNode taintedFile2, + DataFlow2::PathNode infoLeak, InformationLeakConfig infoLeakConf, + TaintedPathConfig taintedPathConf //, PathCreation p +where + taintedPathConf.hasFlowPath(remoteSource, taintedFile) and + taintedFile.getNode() = taintedFile2.getNode() and + infoLeakConf.hasFlowPath(taintedFile2, infoLeak) +select infoLeak.getNode(), taintedFile2, infoLeak, + "Potential disclosure of arbitrary file due to $@ derived from $@.", + taintedFile2.getNode().(TaintedPathSink).getTaintedFileInput(), "user-provided value", + remoteSource.getNode(), "a remote source" diff --git a/java/ql/src/experimental/Security/CWE/CWE-706/UserControlledArbitraryReadWrite.ql b/java/ql/src/experimental/Security/CWE/CWE-706/UserControlledArbitraryReadWrite.ql index 6d43ccb2eaa1..abdb20da0477 100644 --- a/java/ql/src/experimental/Security/CWE/CWE-706/UserControlledArbitraryReadWrite.ql +++ b/java/ql/src/experimental/Security/CWE/CWE-706/UserControlledArbitraryReadWrite.ql @@ -1,10 +1,10 @@ /** - * @name User-controlled read/write on user-controlled path expression - * @description Reading/writing from/to paths influenced by users can allow an attacker to read or write attacker-controlled content to an arbitrary resources . + * @name Disclosure of user-controlled path expression + * @description Disclosing content from paths influenced by users can allow an attacker to read arbitrary resources. * @kind path-problem * @problem.severity error - * @precision high - * @id java/tainted-file-read-write + * @precision medium + * @id java/tainted-file-read * @tags security * external/cwe/cwe-706 */ @@ -12,21 +12,10 @@ import java import semmle.code.java.dataflow.FlowSources import semmle.code.java.dataflow.TaintTracking2 -import semmle.code.java.dataflow.TaintTracking import semmle.code.java.security.XSS import DataFlow2::PathGraph import PathsCommon -/** The class `java.io.FileInputStream`. */ -class TypeFileInputStream extends Class { - TypeFileInputStream() { this.hasQualifiedName("java.io", "FileInputStream") } -} - -/** The class `java.nio.file.Files`. */ -class TypeFiles extends Class { - TypeFiles() { this.hasQualifiedName("java.nio.file", "Files") } -} - /** The class `org.json.JSONObject`. */ class TypeJsonObject extends Class { TypeJsonObject() { this.hasQualifiedName("org.json", "JSONObject") } @@ -108,7 +97,7 @@ class SensitiveFileOperationSink extends DataFlow::ExprNode { } } -/** Holds if `node1` is used in the creation of `node2` and not guarded. */ +/** Holds if `node1` is used in the creation of `node2` and not guarded. */ predicate usedInPathCreation(DataFlow::Node node1, DataFlow::Node node2) { exists(Expr e | e = node1.asExpr() | e = node2.asExpr().(PathCreation).getInput() and not guarded(e) @@ -116,10 +105,9 @@ predicate usedInPathCreation(DataFlow::Node node1, DataFlow::Node node2) { } predicate putsValueIntoJsonObject(DataFlow::Node node1, DataFlow::Node node2) { - exists(MethodAccess call, string methodName | + exists(MethodAccess call | call.getReceiverType() instanceof TypeJsonObject and - call.getMethod().getName() = methodName and - (methodName = "put" or methodName = "putOnce" or methodName = "putOpt") and + call.getMethod().getName() = ["put", "putOnce", "putOpt"] and call.getQualifier() = node2.asExpr() and call.getArgument(1) = node1.asExpr() ) @@ -138,49 +126,6 @@ predicate putsValueIntoJsonArray(DataFlow::Node node1, DataFlow::Node node2) { ) } -predicate isFileToPath(DataFlow::Node node1, DataFlow::Node node2) { - exists(MethodAccess call | - call.getReceiverType() instanceof TypeFile and - call.getMethod().hasName("toPath") and - call = node2.asExpr() and - call.getQualifier() = node1.asExpr() - ) -} - -predicate isPathToFile(DataFlow::Node node1, DataFlow::Node node2) { - exists(MethodAccess call | - call.getReceiverType() instanceof TypePath and - call.getMethod().hasName("toFile") and - call = node2.asExpr() and - call.getQualifier() = node1.asExpr() - ) -} - -predicate readsAllBytesFromPath(DataFlow::Node node1, DataFlow::Node node2) { - exists(MethodAccess call | - call.getReceiverType() instanceof TypeFiles and - (call.getMethod().hasName("readAllBytes") or call.getMethod().hasName("readAllLines")) and - call = node2.asExpr() and - call.getArgument(0) = node1.asExpr() - ) -} - -predicate inputStreamReadsFromFile(DataFlow::Node node1, DataFlow::Node node2) { - exists(ConstructorCall call | - call.getConstructedType() instanceof TypeFileInputStream and - call = node2.asExpr() and - call.getAnArgument() = node1.asExpr() - ) -} - -predicate taintedNewFile(DataFlow::Node node1, DataFlow::Node node2) { - exists(ConstructorCall call | - call.getConstructedType() instanceof TypeFile and - call = node2.asExpr() and - call.getAnArgument() = node1.asExpr() - ) -} - class ContainsDotDotSanitizer extends DataFlow::BarrierGuard { ContainsDotDotSanitizer() { this.(MethodAccess).getMethod().hasName("contains") and @@ -197,10 +142,7 @@ class TaintedPathConfig extends TaintTracking::Configuration { override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource } - override predicate isSink(DataFlow::Node sink) { - exists(TaintedPathSink s | s.getTaintedFile() = sink.asExpr()) - // sink instanceof TaintedPathSink - } + override predicate isSink(DataFlow::Node sink) { sink instanceof TaintedPathSink } override predicate isSanitizer(DataFlow::Node node) { exists(Type t | t = node.getType() | t instanceof BoxedType or t instanceof PrimitiveType) @@ -208,6 +150,7 @@ class TaintedPathConfig extends TaintTracking::Configuration { override predicate isSanitizerGuard(DataFlow::BarrierGuard guard) { guard instanceof ContainsDotDotSanitizer + // TODO add guards from zipslip.ql } override predicate isAdditionalTaintStep(DataFlow::Node node1, DataFlow::Node node2) { @@ -217,19 +160,25 @@ class TaintedPathConfig extends TaintTracking::Configuration { private class TaintedPathSink extends DataFlow::Node { Expr path; + Expr taintedInput; TaintedPathSink() { - exists(Expr e, PathCreation p | e = asExpr() | e = p.getInput() and not guarded(e) and path = p) + exists(Expr e, PathCreation p | e = asExpr() | + e = p.getInput() and not guarded(e) and path = p and taintedInput = e + ) } Expr getTaintedFile() { result = path } + + Expr getTaintedFileInput() { result = taintedInput } } class InformationLeakConfig extends TaintTracking2::Configuration { InformationLeakConfig() { this = "InformationLeakConfig" } override predicate isSource(DataFlow::Node source) { - exists(TaintedPathSink s | s.getTaintedFile() = source.asExpr()) + source instanceof TaintedPathSink + //exists(TaintedPathSink s | s.getTaintedFile() = source.asExpr()) //source instanceof TaintedPathSink //any() //source.asExpr().getType() instanceof TypePath //any()//source instanceof RemoteFlowSource } //source.asExpr().getFile().getBaseName().matches("GetSkillJsonService.java")}//any()}//source instanceof RemoteFlowSource } @@ -239,7 +188,7 @@ class InformationLeakConfig extends TaintTracking2::Configuration { or //sink instanceof ServiceResponseSink or sink instanceof XssSink //or - //sink instanceof SensitiveFileOperationSink + // sink instanceof SensitiveFileOperationSink } override predicate isSanitizer(DataFlow::Node node) { @@ -247,27 +196,15 @@ class InformationLeakConfig extends TaintTracking2::Configuration { } override predicate isAdditionalTaintStep(DataFlow::Node node1, DataFlow::Node node2) { - // none() - // usedInPathCreation(node1, node2) - //or - inputStreamReadsFromFile(node1, node2) - or + usedInPathCreation(node1, node2) /* * or - * isFileToPath(node1, node2) - * or - * isPathToFile(node1, node2) - * or - * readsAllBytesFromPath(node1, node2) - * or - * putsValueIntoJsonObject(node1, node2) - * or - * putsValueIntoJsonArray(node1, node2) + * putsValueIntoJsonObject(node1, node2) + * or + * putsValueIntoJsonArray(node1, node2) */ - taintedNewFile(node1, node2) - /* */ - } + } } from @@ -277,8 +214,8 @@ from where taintedPathConf.hasFlowPath(remoteSource, taintedFile) and taintedFile.getNode() = taintedFile2.getNode() and - //p = taintedFile2.getNode().asExpr() and - //TaintTracking::localExprTaint(p, taintedFile2.getNode().asExpr()) and infoLeakConf.hasFlowPath(taintedFile2, infoLeak) -select infoLeak.getNode(), taintedFile2, infoLeak, "Information leak due to $@.", - taintedFile2.getNode(), "user-provided value" +select infoLeak.getNode(), taintedFile2, infoLeak, + "Potential disclosure of arbitrary file due to $@ derived from $@.", + taintedFile2.getNode().(TaintedPathSink).getTaintedFileInput(), "user-provided value", + remoteSource.getNode(), "a remote source" diff --git a/java/ql/src/experimental/Security/CWE/CWE-706/UserControlledArbitraryWrite.ql b/java/ql/src/experimental/Security/CWE/CWE-706/UserControlledArbitraryWrite.ql new file mode 100644 index 000000000000..ab75c7c50bbc --- /dev/null +++ b/java/ql/src/experimental/Security/CWE/CWE-706/UserControlledArbitraryWrite.ql @@ -0,0 +1,221 @@ +/** + * @name User-controlled content written on user-controlled path expression. + * @description Writing user-controlled data to an user-controlled paths can allow an attacker to write arbitrary files. + * @kind path-problem + * @problem.severity error + * @precision medium + * @id java/tainted-file-write + * @tags security + * external/cwe/cwe-706 + */ + +import java +import semmle.code.java.dataflow.FlowSources +import semmle.code.java.dataflow.TaintTracking2 +import semmle.code.java.security.XSS +import DataFlow2::PathGraph +import PathsCommon + +/** The class `org.json.JSONObject`. */ +class TypeJsonObject extends Class { + TypeJsonObject() { this.hasQualifiedName("org.json", "JSONObject") } +} + +/** The class `org.json.JSONArray`. */ +class TypeJsonArray extends Class { + TypeJsonArray() { this.hasQualifiedName("org.json", "JSONArray") } +} + +/** The class `ai.susi.server.ServiceResponse`. */ +class TypeServiceResponse extends Class { + TypeServiceResponse() { this.hasQualifiedName("ai.susi.server", "ServiceResponse") } +} + +class ServiceResponseSink extends DataFlow::ExprNode { + ServiceResponseSink() { + exists(ConstructorCall call | + call.getConstructedType() instanceof TypeServiceResponse and + this.getExpr() = call.getAnArgument() + ) + or + exists(MethodAccess call | + call.getType() instanceof TypeServiceResponse and + this.getExpr() = call.getAnArgument() + ) + } +} + +predicate deletesFile(DataFlow::ExprNode node) { + exists(MethodAccess call | + call.getReceiverType() instanceof TypeFile and + call.getMethod().getName().matches("delete%") and + node.getExpr() = call.getQualifier() + ) +} + +predicate deletesPath(DataFlow::ExprNode node) { + exists(MethodAccess call | + call.getReceiverType() instanceof TypeFiles and + call.getMethod().getName().matches("delete%") and + node.getExpr() = call.getArgument(0) + ) +} + +predicate renamesFile(DataFlow::ExprNode node) { + exists(MethodAccess call | + call.getReceiverType() instanceof TypeFile and + call.getMethod().getName().matches("renameTo%") and + ( + node.getExpr() = call.getQualifier() + or + node.getExpr() = call.getArgument(0) + ) + ) +} + +predicate renamesPath(DataFlow::ExprNode node) { + exists(MethodAccess call | + call.getReceiverType() instanceof TypeFiles and + call.getMethod().getName().matches("move%") and + ( + node.getExpr() = call.getArgument(0) + or + node.getExpr() = call.getArgument(1) + ) + ) +} + +class SensitiveFileOperationSink extends DataFlow::ExprNode { + SensitiveFileOperationSink() { + deletesFile(this) + or + deletesPath(this) + or + renamesFile(this) + or + renamesPath(this) + } +} + +/** Holds if `node1` is used in the creation of `node2` and not guarded. */ +predicate usedInPathCreation(DataFlow::Node node1, DataFlow::Node node2) { + exists(Expr e | e = node1.asExpr() | + e = node2.asExpr().(PathCreation).getInput() and not guarded(e) + ) +} + +predicate putsValueIntoJsonObject(DataFlow::Node node1, DataFlow::Node node2) { + exists(MethodAccess call | + call.getReceiverType() instanceof TypeJsonObject and + call.getMethod().getName() = ["put", "putOnce", "putOpt"] and + call.getQualifier() = node2.asExpr() and + call.getArgument(1) = node1.asExpr() + ) +} + +predicate putsValueIntoJsonArray(DataFlow::Node node1, DataFlow::Node node2) { + exists(MethodAccess call | + call.getReceiverType() instanceof TypeJsonArray and + call.getMethod().getName() = "put" and + call.getQualifier() = node2.asExpr() and + ( + call.getArgument(1) = node1.asExpr() and call.getNumArgument() = 2 + or + call.getArgument(0) = node1.asExpr() and call.getNumArgument() = 1 + ) + ) +} + +class ContainsDotDotSanitizer extends DataFlow::BarrierGuard { + ContainsDotDotSanitizer() { + this.(MethodAccess).getMethod().hasName("contains") and + this.(MethodAccess).getAnArgument().(StringLiteral).getValue() = ".." + } + + override predicate checks(Expr e, boolean branch) { + e = this.(MethodAccess).getQualifier() and branch = false + } +} + +class TaintedPathConfig extends TaintTracking::Configuration { + TaintedPathConfig() { this = "TaintedPathConfig" } + + override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource } + + override predicate isSink(DataFlow::Node sink) { sink instanceof TaintedPathSink } + + override predicate isSanitizer(DataFlow::Node node) { + exists(Type t | t = node.getType() | t instanceof BoxedType or t instanceof PrimitiveType) + } + + override predicate isSanitizerGuard(DataFlow::BarrierGuard guard) { + guard instanceof ContainsDotDotSanitizer + // TODO add guards from zipslip.ql + } + + override predicate isAdditionalTaintStep(DataFlow::Node node1, DataFlow::Node node2) { + usedInPathCreation(node1, node2) + } +} + +private class TaintedPathSink extends DataFlow::Node { + Expr path; + Expr taintedInput; + + TaintedPathSink() { + exists(Expr e, PathCreation p | e = asExpr() | + e = p.getInput() and not guarded(e) and path = p and taintedInput = e + ) + } + + Expr getTaintedFile() { result = path } + + Expr getTaintedFileInput() { result = taintedInput } +} + +class InformationLeakConfig extends TaintTracking2::Configuration { + InformationLeakConfig() { this = "InformationLeakConfig" } + + override predicate isSource(DataFlow::Node source) { + source instanceof TaintedPathSink + //exists(TaintedPathSink s | s.getTaintedFile() = source.asExpr()) + //source instanceof TaintedPathSink + //any() //source.asExpr().getType() instanceof TypePath //any()//source instanceof RemoteFlowSource + } //source.asExpr().getFile().getBaseName().matches("GetSkillJsonService.java")}//any()}//source instanceof RemoteFlowSource } + + override predicate isSink(DataFlow::Node sink) { + sink instanceof RemoteFlowSink + or + //sink instanceof ServiceResponseSink or + sink instanceof XssSink //or + // sink instanceof SensitiveFileOperationSink + } + + override predicate isSanitizer(DataFlow::Node node) { + node.getType() instanceof NumericType or node.getType() instanceof BooleanType + } + + override predicate isAdditionalTaintStep(DataFlow::Node node1, DataFlow::Node node2) { + usedInPathCreation(node1, node2) + /* + * or + * putsValueIntoJsonObject(node1, node2) + * or + * putsValueIntoJsonArray(node1, node2) + */ + + } +} + +from + DataFlow::PathNode remoteSource, DataFlow::PathNode taintedFile, DataFlow2::PathNode taintedFile2, + DataFlow2::PathNode infoLeak, InformationLeakConfig infoLeakConf, + TaintedPathConfig taintedPathConf //, PathCreation p +where + taintedPathConf.hasFlowPath(remoteSource, taintedFile) and + taintedFile.getNode() = taintedFile2.getNode() and + infoLeakConf.hasFlowPath(taintedFile2, infoLeak) +select infoLeak.getNode(), taintedFile2, infoLeak, + "Potential disclosure of arbitrary file due to $@ derived from $@.", + taintedFile2.getNode().(TaintedPathSink).getTaintedFileInput(), "user-provided value", + remoteSource.getNode(), "a remote source" From 064c28726201cb18bba9195b3887665d863d8a3b Mon Sep 17 00:00:00 2001 From: intrigus Date: Thu, 2 Jul 2020 23:06:44 +0200 Subject: [PATCH 07/11] Create `PathCreation` library for easier reuse. --- .../src/Security/CWE/CWE-022/TaintedPath.ql | 2 +- .../Security/CWE/CWE-022/TaintedPathLocal.ql | 2 +- .../Security/CWE/CWE-706/PathsCommon.qll | 149 ------------------ .../CWE-706/UserControlledArbitraryRead.ql | 10 +- .../CWE-706/UserControlledArbitraryWrite.ql | 59 ++++--- .../code/java/security/PathCreation.qll} | 12 +- 6 files changed, 55 insertions(+), 179 deletions(-) delete mode 100644 java/ql/src/experimental/Security/CWE/CWE-706/PathsCommon.qll rename java/ql/src/{Security/CWE/CWE-022/PathsCommon.qll => semmle/code/java/security/PathCreation.qll} (81%) diff --git a/java/ql/src/Security/CWE/CWE-022/TaintedPath.ql b/java/ql/src/Security/CWE/CWE-022/TaintedPath.ql index 2094207dc92b..0911436e8932 100644 --- a/java/ql/src/Security/CWE/CWE-022/TaintedPath.ql +++ b/java/ql/src/Security/CWE/CWE-022/TaintedPath.ql @@ -14,7 +14,7 @@ import java import semmle.code.java.dataflow.FlowSources -import PathsCommon +import semmle.code.java.security.PathCreation import DataFlow::PathGraph class ContainsDotDotSanitizer extends DataFlow::BarrierGuard { diff --git a/java/ql/src/Security/CWE/CWE-022/TaintedPathLocal.ql b/java/ql/src/Security/CWE/CWE-022/TaintedPathLocal.ql index 4d1c20f923e4..cd65a7567583 100644 --- a/java/ql/src/Security/CWE/CWE-022/TaintedPathLocal.ql +++ b/java/ql/src/Security/CWE/CWE-022/TaintedPathLocal.ql @@ -14,7 +14,7 @@ import java import semmle.code.java.dataflow.FlowSources -import PathsCommon +import semmle.code.java.security.PathCreation import DataFlow::PathGraph class TaintedPathLocalConfig extends TaintTracking::Configuration { diff --git a/java/ql/src/experimental/Security/CWE/CWE-706/PathsCommon.qll b/java/ql/src/experimental/Security/CWE/CWE-706/PathsCommon.qll deleted file mode 100644 index 8dfd5bc096d7..000000000000 --- a/java/ql/src/experimental/Security/CWE/CWE-706/PathsCommon.qll +++ /dev/null @@ -1,149 +0,0 @@ -import java -import semmle.code.java.controlflow.Guards -import semmle.code.java.dataflow.DataFlow -import semmle.code.java.dataflow.TaintTracking - -abstract class PathCreation extends Expr { - /** Gets an input that is used in the creation of this path. */ - abstract Expr getInput(); -} - -/** Models the `java.nio.file.Paths.get` method. */ -class PathsGet extends PathCreation, MethodAccess { - PathsGet() { - exists(Method m | m = this.getMethod() | - m.getDeclaringType() instanceof TypePaths and - m.getName() = "get" - ) - } - - override Expr getInput() { result = this.getAnArgument() } -} - -/** Models the `java.nio.file.FileSystem.getPath` method. */ -class FileSystemGetPath extends PathCreation, MethodAccess { - FileSystemGetPath() { - exists(Method m | m = this.getMethod() | - m.getDeclaringType() instanceof TypeFileSystem and - m.getName() = "getPath" - ) - } - - override Expr getInput() { result = this.getAnArgument() } -} - -/** Models the `new java.io.File(...)` constructor. */ -class FileCreation extends PathCreation, ClassInstanceExpr { - FileCreation() { this.getConstructedType() instanceof TypeFile } - - override Expr getInput() { - result = this.getAnArgument() and - // Relevant arguments include those that are not a `File`. - not result.getType() instanceof TypeFile - } -} - -/** Models the `new java.io.FileWriter(...)` constructor. */ -class FileWriterCreation extends PathCreation, ClassInstanceExpr { - FileWriterCreation() { this.getConstructedType().getQualifiedName() = "java.io.FileWriter" } - - override Expr getInput() { - result = this.getAnArgument() and - // Relevant arguments are those of type `String`. - result.getType() instanceof TypeString - } -} - -predicate inWeakCheck(Expr e) { - // None of these are sufficient to guarantee that a string is safe. - exists(MethodAccess m, Method def | m.getQualifier() = e and m.getMethod() = def | - def.getName() = "startsWith" or - def.getName() = "endsWith" or - def.getName() = "isEmpty" or - def.getName() = "equals" - ) - or - // Checking against `null` has no bearing on path traversal. - exists(EqualityTest b | b.getAnOperand() = e | b.getAnOperand() instanceof NullLiteral) -} - -// Ignore cases where the variable has been checked somehow, -// but allow some particularly obviously bad cases. -predicate guarded(VarAccess e) { - exists(PathCreation p | e = p.getInput()) and - exists(ConditionBlock cb, Expr c | - cb.getCondition().getAChildExpr*() = c and - c = e.getVariable().getAnAccess() and - cb.controls(e.getBasicBlock(), true) and - // Disallow a few obviously bad checks. - not inWeakCheck(c) - ) -} - -/** The class `java.io.FileInputStream`. */ -class TypeFileInputStream extends Class { - TypeFileInputStream() { this.hasQualifiedName("java.io", "FileInputStream") } -} - -/** Models additional taint steps like `file.toPath()`, `path.toFile()`, `new FileInputStream(..)`, `Files.readAll{Bytes|Lines}(...)`, and `new File(...)`. */ -class PathAdditionalTaintStep extends TaintTracking::AdditionalTaintStep { - override predicate step(DataFlow::Node node1, DataFlow::Node node2) { - inputStreamReadsFromFile(node1, node2) - or - isFileToPath(node1, node2) - or - isPathToFile(node1, node2) - or - readsAllFromPath(node1, node2) - or - taintedNewFile(node1, node2) - } -} - -/** Holds if `node1` is converted to `node2` via a call to `node1.toPath()`. */ -private predicate isFileToPath(DataFlow::Node node1, DataFlow::Node node2) { - exists(MethodAccess call | - call.getReceiverType() instanceof TypeFile and - call.getMethod().hasName("toPath") and - call = node2.asExpr() and - call.getQualifier() = node1.asExpr() - ) -} - -/** Holds if `node1` is converted to `node2` via a call to `node1.toFile()`. */ -private predicate isPathToFile(DataFlow::Node node1, DataFlow::Node node2) { - exists(MethodAccess call | - call.getReceiverType() instanceof TypePath and - call.getMethod().hasName("toFile") and - call = node2.asExpr() and - call.getQualifier() = node1.asExpr() - ) -} - -/** Holds if `node1` is read by `node2` via a call to `Files.readAllBytes(node1)` or `Files.readAllLines(node1)`. */ -private predicate readsAllFromPath(DataFlow::Node node1, DataFlow::Node node2) { - exists(MethodAccess call | - call.getReceiverType() instanceof TypeFiles and - call.getMethod().hasName(["readAllBytes", "readAllLines"]) and - call = node2.asExpr() and - call.getArgument(0) = node1.asExpr() - ) -} - -/** Holds if `node1` is passed to `node2` via a call to `new FileInputStream(node1)`. */ -private predicate inputStreamReadsFromFile(DataFlow::Node node1, DataFlow::Node node2) { - exists(ConstructorCall call | - call.getConstructedType() instanceof TypeFileInputStream and - call = node2.asExpr() and - call.getAnArgument() = node1.asExpr() - ) -} - -/** Holds if `node1` is passed to `node2` via a call to `new File(node1)`. */ -private predicate taintedNewFile(DataFlow::Node node1, DataFlow::Node node2) { - exists(ConstructorCall call | - call.getConstructedType() instanceof TypeFile and - call = node2.asExpr() and - call.getAnArgument() = node1.asExpr() - ) -} diff --git a/java/ql/src/experimental/Security/CWE/CWE-706/UserControlledArbitraryRead.ql b/java/ql/src/experimental/Security/CWE/CWE-706/UserControlledArbitraryRead.ql index abdb20da0477..fd669634fc3e 100644 --- a/java/ql/src/experimental/Security/CWE/CWE-706/UserControlledArbitraryRead.ql +++ b/java/ql/src/experimental/Security/CWE/CWE-706/UserControlledArbitraryRead.ql @@ -14,7 +14,7 @@ import semmle.code.java.dataflow.FlowSources import semmle.code.java.dataflow.TaintTracking2 import semmle.code.java.security.XSS import DataFlow2::PathGraph -import PathsCommon +import semmle.code.java.security.PathCreation /** The class `org.json.JSONObject`. */ class TypeJsonObject extends Class { @@ -137,7 +137,7 @@ class ContainsDotDotSanitizer extends DataFlow::BarrierGuard { } } -class TaintedPathConfig extends TaintTracking::Configuration { +class TaintedPathConfig extends TaintTracking2::Configuration { TaintedPathConfig() { this = "TaintedPathConfig" } override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource } @@ -208,9 +208,9 @@ class InformationLeakConfig extends TaintTracking2::Configuration { } from - DataFlow::PathNode remoteSource, DataFlow::PathNode taintedFile, DataFlow2::PathNode taintedFile2, - DataFlow2::PathNode infoLeak, InformationLeakConfig infoLeakConf, - TaintedPathConfig taintedPathConf //, PathCreation p + DataFlow2::PathNode remoteSource, DataFlow2::PathNode taintedFile, + DataFlow2::PathNode taintedFile2, DataFlow2::PathNode infoLeak, + InformationLeakConfig infoLeakConf, TaintedPathConfig taintedPathConf where taintedPathConf.hasFlowPath(remoteSource, taintedFile) and taintedFile.getNode() = taintedFile2.getNode() and diff --git a/java/ql/src/experimental/Security/CWE/CWE-706/UserControlledArbitraryWrite.ql b/java/ql/src/experimental/Security/CWE/CWE-706/UserControlledArbitraryWrite.ql index ab75c7c50bbc..651627215256 100644 --- a/java/ql/src/experimental/Security/CWE/CWE-706/UserControlledArbitraryWrite.ql +++ b/java/ql/src/experimental/Security/CWE/CWE-706/UserControlledArbitraryWrite.ql @@ -137,7 +137,7 @@ class ContainsDotDotSanitizer extends DataFlow::BarrierGuard { } } -class TaintedPathConfig extends TaintTracking::Configuration { +class TaintedPathConfig extends TaintTracking2::Configuration { TaintedPathConfig() { this = "TaintedPathConfig" } override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource } @@ -173,23 +173,17 @@ private class TaintedPathSink extends DataFlow::Node { Expr getTaintedFileInput() { result = taintedInput } } -class InformationLeakConfig extends TaintTracking2::Configuration { - InformationLeakConfig() { this = "InformationLeakConfig" } +class UserControlledWriteConfig extends TaintTracking2::Configuration { + UserControlledWriteConfig() { this = "UserControlledWriteConfig" } override predicate isSource(DataFlow::Node source) { - source instanceof TaintedPathSink + source instanceof RemoteFlowSource //exists(TaintedPathSink s | s.getTaintedFile() = source.asExpr()) //source instanceof TaintedPathSink //any() //source.asExpr().getType() instanceof TypePath //any()//source instanceof RemoteFlowSource } //source.asExpr().getFile().getBaseName().matches("GetSkillJsonService.java")}//any()}//source instanceof RemoteFlowSource } - override predicate isSink(DataFlow::Node sink) { - sink instanceof RemoteFlowSink - or - //sink instanceof ServiceResponseSink or - sink instanceof XssSink //or - // sink instanceof SensitiveFileOperationSink - } + override predicate isSink(DataFlow::Node sink) { sink instanceof FileWriteSink } override predicate isSanitizer(DataFlow::Node node) { node.getType() instanceof NumericType or node.getType() instanceof BooleanType @@ -204,18 +198,39 @@ class InformationLeakConfig extends TaintTracking2::Configuration { * putsValueIntoJsonArray(node1, node2) */ - } + } +} + +/** Holds if `content` is written to `file`. */ +private predicate writesToFile(Expr content, Expr file) { + exists(MethodAccess ma | + ma.getMethod().hasName("write") and + ma.getMethod().getDeclaringType().hasQualifiedName(_, "OutputStream") + | + derivedFromFile(ma.getQualifier(), file) and ma.getAnArgument() = content + ) +} + +private predicate derivedFromFile(Expr e, Expr file) { + file.getType() instanceof TypeFile and TaintTracking::localExprTaint(file, e) and e.getType().(RefType).hasQualifiedName(_, "FileOutputStream") +} + +class FileWriteSink extends DataFlow::Node { + Expr file; + + FileWriteSink() { writesToFile(this.asExpr(), file) } + + Expr getTaintedFile() { result = file } } from - DataFlow::PathNode remoteSource, DataFlow::PathNode taintedFile, DataFlow2::PathNode taintedFile2, - DataFlow2::PathNode infoLeak, InformationLeakConfig infoLeakConf, - TaintedPathConfig taintedPathConf //, PathCreation p + DataFlow2::PathNode remoteFileCreationSource, DataFlow2::PathNode remoteContentSource, + DataFlow2::PathNode taintedFile, DataFlow2::PathNode infoLeak, + UserControlledWriteConfig taintedWriteConf, TaintedPathConfig taintedPathConf where - taintedPathConf.hasFlowPath(remoteSource, taintedFile) and - taintedFile.getNode() = taintedFile2.getNode() and - infoLeakConf.hasFlowPath(taintedFile2, infoLeak) -select infoLeak.getNode(), taintedFile2, infoLeak, - "Potential disclosure of arbitrary file due to $@ derived from $@.", - taintedFile2.getNode().(TaintedPathSink).getTaintedFileInput(), "user-provided value", - remoteSource.getNode(), "a remote source" + taintedPathConf.hasFlowPath(remoteFileCreationSource, taintedFile) and + taintedWriteConf.hasFlowPath(remoteContentSource, taintedFile) +select infoLeak.getNode(), taintedFile, infoLeak, + "Potential $@ written to $@ file derived from $@.", remoteContentSource, + "user-controlled content", taintedFile.getNode().(TaintedPathSink).getTaintedFileInput(), + "an user-controlled", remoteFileCreationSource.getNode(), "a remote source" diff --git a/java/ql/src/Security/CWE/CWE-022/PathsCommon.qll b/java/ql/src/semmle/code/java/security/PathCreation.qll similarity index 81% rename from java/ql/src/Security/CWE/CWE-022/PathsCommon.qll rename to java/ql/src/semmle/code/java/security/PathCreation.qll index 362144d8c191..6832a2033871 100644 --- a/java/ql/src/Security/CWE/CWE-022/PathsCommon.qll +++ b/java/ql/src/semmle/code/java/security/PathCreation.qll @@ -1,10 +1,17 @@ +/** + * Models the different ways to create paths. Either by using `java.io.File`-related APIs or `java.nio.Path`-related APIs. + */ + import java import semmle.code.java.controlflow.Guards +/** Models the creation of a path. */ abstract class PathCreation extends Expr { + /** Gets an input that is used in the creation of this path. */ abstract Expr getInput(); } +/** Models the `java.nio.file.Paths.get` method. */ class PathsGet extends PathCreation, MethodAccess { PathsGet() { exists(Method m | m = this.getMethod() | @@ -16,6 +23,7 @@ class PathsGet extends PathCreation, MethodAccess { override Expr getInput() { result = this.getAnArgument() } } +/** Models the `java.nio.file.FileSystem.getPath` method. */ class FileSystemGetPath extends PathCreation, MethodAccess { FileSystemGetPath() { exists(Method m | m = this.getMethod() | @@ -27,6 +35,7 @@ class FileSystemGetPath extends PathCreation, MethodAccess { override Expr getInput() { result = this.getAnArgument() } } +/** Models the `new java.io.File(...)` constructor. */ class FileCreation extends PathCreation, ClassInstanceExpr { FileCreation() { this.getConstructedType() instanceof TypeFile } @@ -37,6 +46,7 @@ class FileCreation extends PathCreation, ClassInstanceExpr { } } +/** Models the `new java.io.FileWriter(...)` constructor. */ class FileWriterCreation extends PathCreation, ClassInstanceExpr { FileWriterCreation() { this.getConstructedType().getQualifiedName() = "java.io.FileWriter" } @@ -47,7 +57,7 @@ class FileWriterCreation extends PathCreation, ClassInstanceExpr { } } -predicate inWeakCheck(Expr e) { +private predicate inWeakCheck(Expr e) { // None of these are sufficient to guarantee that a string is safe. exists(MethodAccess m, Method def | m.getQualifier() = e and m.getMethod() = def | def.getName() = "startsWith" or From 201779d67248502545bb46678b0d827275dac1b1 Mon Sep 17 00:00:00 2001 From: intrigus Date: Thu, 2 Jul 2020 23:25:05 +0200 Subject: [PATCH 08/11] Model additional path creation apis. --- .../code/java/security/PathCreation.qll | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/java/ql/src/semmle/code/java/security/PathCreation.qll b/java/ql/src/semmle/code/java/security/PathCreation.qll index 6832a2033871..c78b35251f62 100644 --- a/java/ql/src/semmle/code/java/security/PathCreation.qll +++ b/java/ql/src/semmle/code/java/security/PathCreation.qll @@ -46,6 +46,50 @@ class FileCreation extends PathCreation, ClassInstanceExpr { } } +/** Models the `java.nio.Path.resolveSibling` method. */ +class PathResolveSiblingCreation extends PathCreation, MethodAccess { + PathResolveSiblingCreation() { + exists(Method m | m = this.getMethod() | + m.getDeclaringType() instanceof TypePath and + m.getName() = "resolveSibling" + ) + } + + override Expr getInput() { + result = this.getAnArgument() and + // Relevant arguments are those of type `String`. + result.getType() instanceof TypeString + } +} + +/** Models the `java.nio.Path.resolve` method. */ +class PathResolveCreation extends PathCreation, MethodAccess { + PathResolveCreation() { + exists(Method m | m = this.getMethod() | + m.getDeclaringType() instanceof TypePath and + m.getName() = "resolve" + ) + } + + override Expr getInput() { + result = this.getAnArgument() and + // Relevant arguments are those of type `String`. + result.getType() instanceof TypeString + } +} + +/** Models the `java.nio.Path.of` method. */ +class PathOfCreation extends PathCreation, MethodAccess { + PathOfCreation() { + exists(Method m | m = this.getMethod() | + m.getDeclaringType() instanceof TypePath and + m.getName() = "of" + ) + } + + override Expr getInput() { result = this.getAnArgument() } +} + /** Models the `new java.io.FileWriter(...)` constructor. */ class FileWriterCreation extends PathCreation, ClassInstanceExpr { FileWriterCreation() { this.getConstructedType().getQualifiedName() = "java.io.FileWriter" } From 544b179734252879348aab577c8853ef780a4e56 Mon Sep 17 00:00:00 2001 From: intrigus Date: Thu, 2 Jul 2020 23:25:58 +0200 Subject: [PATCH 09/11] Move extra steps to extra file. --- .../Security/CWE/CWE-706/PathsExtraTaint.qll | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 java/ql/src/experimental/Security/CWE/CWE-706/PathsExtraTaint.qll diff --git a/java/ql/src/experimental/Security/CWE/CWE-706/PathsExtraTaint.qll b/java/ql/src/experimental/Security/CWE/CWE-706/PathsExtraTaint.qll new file mode 100644 index 000000000000..271fbd95f628 --- /dev/null +++ b/java/ql/src/experimental/Security/CWE/CWE-706/PathsExtraTaint.qll @@ -0,0 +1,71 @@ +import java +import semmle.code.java.dataflow.DataFlow +import semmle.code.java.dataflow.TaintTracking + +/** The class `java.io.FileInputStream`. */ +class TypeFileInputStream extends Class { + TypeFileInputStream() { this.hasQualifiedName("java.io", "FileInputStream") } +} + +/** Models additional taint steps like `file.toPath()`, `path.toFile()`, `new FileInputStream(..)`, `Files.readAll{Bytes|Lines}(...)`, and `new File(...)`. */ +class PathAdditionalTaintStep extends TaintTracking::AdditionalTaintStep { + override predicate step(DataFlow::Node node1, DataFlow::Node node2) { + inputStreamReadsFromFile(node1, node2) + or + isFileToPath(node1, node2) + or + isPathToFile(node1, node2) + or + readsAllFromPath(node1, node2) + or + taintedNewFile(node1, node2) + } +} + +/** Holds if `node1` is converted to `node2` via a call to `node1.toPath()`. */ +private predicate isFileToPath(DataFlow::Node node1, DataFlow::Node node2) { + exists(MethodAccess call | + call.getReceiverType() instanceof TypeFile and + call.getMethod().hasName("toPath") and + call = node2.asExpr() and + call.getQualifier() = node1.asExpr() + ) +} + +/** Holds if `node1` is converted to `node2` via a call to `node1.toFile()`. */ +private predicate isPathToFile(DataFlow::Node node1, DataFlow::Node node2) { + exists(MethodAccess call | + call.getReceiverType() instanceof TypePath and + call.getMethod().hasName("toFile") and + call = node2.asExpr() and + call.getQualifier() = node1.asExpr() + ) +} + +/** Holds if `node1` is read by `node2` via a call to `Files.readAllBytes(node1)` or `Files.readAllLines(node1)`. */ +private predicate readsAllFromPath(DataFlow::Node node1, DataFlow::Node node2) { + exists(MethodAccess call | + call.getReceiverType() instanceof TypeFiles and + call.getMethod().hasName(["readAllBytes", "readAllLines"]) and + call = node2.asExpr() and + call.getArgument(0) = node1.asExpr() + ) +} + +/** Holds if `node1` is passed to `node2` via a call to `new FileInputStream(node1)`. */ +private predicate inputStreamReadsFromFile(DataFlow::Node node1, DataFlow::Node node2) { + exists(ConstructorCall call | + call.getConstructedType() instanceof TypeFileInputStream and + call = node2.asExpr() and + call.getAnArgument() = node1.asExpr() + ) +} + +/** Holds if `node1` is passed to `node2` via a call to `new File(node1)`. */ +private predicate taintedNewFile(DataFlow::Node node1, DataFlow::Node node2) { + exists(ConstructorCall call | + call.getConstructedType() instanceof TypeFile and + call = node2.asExpr() and + call.getAnArgument() = node1.asExpr() + ) +} From 658f7e508cf9252e01927537705f2e70566ee78e Mon Sep 17 00:00:00 2001 From: intrigus Date: Fri, 3 Jul 2020 00:18:33 +0200 Subject: [PATCH 10/11] Model additional path creation apis. --- .../code/java/security/PathCreation.qll | 39 +++++++++- .../pathcreation/PathCreation.expected | 1 + .../pathcreation/PathCreation.java | 71 +++++++++++++++++++ .../pathcreation/PathCreation.ql | 5 ++ 4 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 java/ql/test/library-tests/pathcreation/PathCreation.expected create mode 100644 java/ql/test/library-tests/pathcreation/PathCreation.java create mode 100644 java/ql/test/library-tests/pathcreation/PathCreation.ql diff --git a/java/ql/src/semmle/code/java/security/PathCreation.qll b/java/ql/src/semmle/code/java/security/PathCreation.qll index c78b35251f62..57d8c76609c4 100644 --- a/java/ql/src/semmle/code/java/security/PathCreation.qll +++ b/java/ql/src/semmle/code/java/security/PathCreation.qll @@ -92,7 +92,44 @@ class PathOfCreation extends PathCreation, MethodAccess { /** Models the `new java.io.FileWriter(...)` constructor. */ class FileWriterCreation extends PathCreation, ClassInstanceExpr { - FileWriterCreation() { this.getConstructedType().getQualifiedName() = "java.io.FileWriter" } + FileWriterCreation() { this.getConstructedType().hasQualifiedName("java.io", "FileWriter") } + + override Expr getInput() { + result = this.getAnArgument() and + // Relevant arguments are those of type `String`. + result.getType() instanceof TypeString + } +} + +/** Models the `new java.io.FileReader(...)` constructor. */ +class FileReaderCreation extends PathCreation, ClassInstanceExpr { + FileReaderCreation() { this.getConstructedType().hasQualifiedName("java.io", "FileReader") } + + override Expr getInput() { + result = this.getAnArgument() and + // Relevant arguments are those of type `String`. + result.getType() instanceof TypeString + } +} + +/** Models the `new java.io.FileInputStream(...)` constructor. */ +class FileInputStreamCreation extends PathCreation, ClassInstanceExpr { + FileInputStreamCreation() { + this.getConstructedType().hasQualifiedName("java.io", "FileInputStream") + } + + override Expr getInput() { + result = this.getAnArgument() and + // Relevant arguments are those of type `String`. + result.getType() instanceof TypeString + } +} + +/** Models the `new java.io.FileOutputStream(...)` constructor. */ +class FileOutputStreamCreation extends PathCreation, ClassInstanceExpr { + FileOutputStreamCreation() { + this.getConstructedType().hasQualifiedName("java.io", "FileOutputStream") + } override Expr getInput() { result = this.getAnArgument() and diff --git a/java/ql/test/library-tests/pathcreation/PathCreation.expected b/java/ql/test/library-tests/pathcreation/PathCreation.expected new file mode 100644 index 000000000000..70ad1b745bdb --- /dev/null +++ b/java/ql/test/library-tests/pathcreation/PathCreation.expected @@ -0,0 +1 @@ +| JSNI.java:5:48:7:8 | /* -{ ... */ | JSNI.java:5:24:5:32 | scrollTo1 | diff --git a/java/ql/test/library-tests/pathcreation/PathCreation.java b/java/ql/test/library-tests/pathcreation/PathCreation.java new file mode 100644 index 000000000000..dcdb28adf457 --- /dev/null +++ b/java/ql/test/library-tests/pathcreation/PathCreation.java @@ -0,0 +1,71 @@ +import java.io.File; +import java.io.FileWriter; +import java.io.FileReader; +import java.io.FileOutputStream; +import java.io.FileInputStream; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.FileSystems; +import java.net.URI; + +class PathCreation { + public void testNewFileWithString() { + File f = new File("dir"); + File f2 = new File("dir", "sub"); + } + + public void testNewFileWithFileString() { + File f = new File(new File("dir"), "sub"); + } + + public void testNewFileWithURI() { + File f = new File(new URI("dir")); + } + + public void testPathOfWithString() { + Path p = Path.of("dir"); + Path p2 = Path.of("dir", "sub"); + } + + public void testPathOfWithURI() { + Path p = Path.of(new URI("dir")); + } + + public void testPathsGetWithString() { + Path p = Paths.get("dir"); + Path p2 = Paths.get("dir", "sub"); + } + + public void testPathsGetWithURI() { + Path p = Paths.get(new URI("dir")); + } + + public void testFileSystemGetPathWithString() { + Path p = FileSystems.getDefault().getPath("dir"); + Path p2 = FileSystems.getDefault().getPath("dir", "sub"); + } + + public void testPathResolveSiblingWithString() { + Path p = Path.of("dir").resolveSibling("sub"); + } + + public void testPathResolveWithString() { + Path p = Path.of("dir").resolve("sub"); + } + + public void testNewFileWriterWithString() { + FileWriter fw = new FileWriter("dir"); + } + + public void testNewFileReaderWithString() { + FileReader fr = new FileReader("dir"); + } + + public void testNewFileOutputStreamWithString() { + FileOutputStream fos = new FileOutputStream("dir"); + } + + public void testNewFileInputStreamWithString() { + FileInputStream fis = new FileInputStream("dir"); + } +} diff --git a/java/ql/test/library-tests/pathcreation/PathCreation.ql b/java/ql/test/library-tests/pathcreation/PathCreation.ql new file mode 100644 index 000000000000..8c949116f4ca --- /dev/null +++ b/java/ql/test/library-tests/pathcreation/PathCreation.ql @@ -0,0 +1,5 @@ +import java +import semmle.code.java.security.PathCreation + +from PathCreation path +select path \ No newline at end of file From 8cea05e53ef1710a5cf4406ca7c490ebc91324e9 Mon Sep 17 00:00:00 2001 From: intrigus Date: Fri, 3 Jul 2020 00:19:00 +0200 Subject: [PATCH 11/11] Accept test changes. --- .../pathcreation/PathCreation.expected | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/java/ql/test/library-tests/pathcreation/PathCreation.expected b/java/ql/test/library-tests/pathcreation/PathCreation.expected index 70ad1b745bdb..0bfd9cdd8937 100644 --- a/java/ql/test/library-tests/pathcreation/PathCreation.expected +++ b/java/ql/test/library-tests/pathcreation/PathCreation.expected @@ -1 +1,21 @@ -| JSNI.java:5:48:7:8 | /* -{ ... */ | JSNI.java:5:24:5:32 | scrollTo1 | +| PathCreation.java:13:18:13:32 | new File(...) | +| PathCreation.java:14:19:14:40 | new File(...) | +| PathCreation.java:18:18:18:49 | new File(...) | +| PathCreation.java:18:27:18:41 | new File(...) | +| PathCreation.java:22:18:22:41 | new File(...) | +| PathCreation.java:26:18:26:31 | of(...) | +| PathCreation.java:27:19:27:39 | of(...) | +| PathCreation.java:31:18:31:40 | of(...) | +| PathCreation.java:35:18:35:33 | get(...) | +| PathCreation.java:36:19:36:41 | get(...) | +| PathCreation.java:40:18:40:42 | get(...) | +| PathCreation.java:44:18:44:56 | getPath(...) | +| PathCreation.java:45:19:45:64 | getPath(...) | +| PathCreation.java:49:18:49:31 | of(...) | +| PathCreation.java:49:18:49:53 | resolveSibling(...) | +| PathCreation.java:53:18:53:31 | of(...) | +| PathCreation.java:53:18:53:46 | resolve(...) | +| PathCreation.java:57:25:57:45 | new FileWriter(...) | +| PathCreation.java:61:25:61:45 | new FileReader(...) | +| PathCreation.java:65:32:65:58 | new FileOutputStream(...) | +| PathCreation.java:69:31:69:56 | new FileInputStream(...) |