diff --git a/java/ql/src/Security/CWE/CWE-022/PathsCommon.qll b/java/ql/src/Security/CWE/CWE-022/PathsCommon.qll deleted file mode 100644 index 362144d8c191..000000000000 --- a/java/ql/src/Security/CWE/CWE-022/PathsCommon.qll +++ /dev/null @@ -1,74 +0,0 @@ -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/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/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/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() + ) +} 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..fd669634fc3e --- /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 semmle.code.java.security.PathCreation + +/** 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 TaintTracking2::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 + 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 + 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 new file mode 100644 index 000000000000..abdb20da0477 --- /dev/null +++ b/java/ql/src/experimental/Security/CWE/CWE-706/UserControlledArbitraryReadWrite.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/UserControlledArbitraryWrite.ql b/java/ql/src/experimental/Security/CWE/CWE-706/UserControlledArbitraryWrite.ql new file mode 100644 index 000000000000..651627215256 --- /dev/null +++ b/java/ql/src/experimental/Security/CWE/CWE-706/UserControlledArbitraryWrite.ql @@ -0,0 +1,236 @@ +/** + * @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 TaintTracking2::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 UserControlledWriteConfig extends TaintTracking2::Configuration { + UserControlledWriteConfig() { this = "UserControlledWriteConfig" } + + override predicate isSource(DataFlow::Node source) { + 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 FileWriteSink } + + 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) + */ + + } +} + +/** 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 + DataFlow2::PathNode remoteFileCreationSource, DataFlow2::PathNode remoteContentSource, + DataFlow2::PathNode taintedFile, DataFlow2::PathNode infoLeak, + UserControlledWriteConfig taintedWriteConf, TaintedPathConfig taintedPathConf +where + 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/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/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/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 diff --git a/java/ql/src/semmle/code/java/security/PathCreation.qll b/java/ql/src/semmle/code/java/security/PathCreation.qll new file mode 100644 index 000000000000..57d8c76609c4 --- /dev/null +++ b/java/ql/src/semmle/code/java/security/PathCreation.qll @@ -0,0 +1,165 @@ +/** + * 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() | + 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 `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().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 + // Relevant arguments are those of type `String`. + result.getType() instanceof TypeString + } +} + +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 + 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/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 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..0bfd9cdd8937 --- /dev/null +++ b/java/ql/test/library-tests/pathcreation/PathCreation.expected @@ -0,0 +1,21 @@ +| 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(...) | 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