Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 0 additions & 14 deletions java/ql/lib/semmle/code/java/dataflow/ExternalFlow.qll
Original file line number Diff line number Diff line change
Expand Up @@ -361,19 +361,7 @@ private class SummaryModelCsvBase extends SummaryModelCsv {
"java.net;URI;false;toURL;;;Argument[-1];ReturnValue;taint;manual",
"java.net;URI;false;toString;;;Argument[-1];ReturnValue;taint;manual",
"java.net;URI;false;toAsciiString;;;Argument[-1];ReturnValue;taint;manual",
"java.io;File;true;toURI;;;Argument[-1];ReturnValue;taint;manual",
"java.io;File;true;toPath;;;Argument[-1];ReturnValue;taint;manual",
"java.io;File;true;getAbsoluteFile;;;Argument[-1];ReturnValue;taint;manual",
"java.io;File;true;getCanonicalFile;;;Argument[-1];ReturnValue;taint;manual",
"java.io;File;true;getAbsolutePath;;;Argument[-1];ReturnValue;taint;manual",
"java.io;File;true;getCanonicalPath;;;Argument[-1];ReturnValue;taint;manual",
"java.nio;ByteBuffer;false;array;();;Argument[-1];ReturnValue;taint;manual",
"java.nio.file;Path;true;normalize;;;Argument[-1];ReturnValue;taint;manual",
"java.nio.file;Path;true;resolve;;;Argument[-1..0];ReturnValue;taint;manual",
"java.nio.file;Path;false;toFile;;;Argument[-1];ReturnValue;taint;manual",
"java.nio.file;Path;true;toString;;;Argument[-1];ReturnValue;taint;manual",
"java.nio.file;Path;true;toUri;;;Argument[-1];ReturnValue;taint;manual",
"java.nio.file;Paths;true;get;;;Argument[0..1];ReturnValue;taint;manual",
"java.io;BufferedReader;true;readLine;;;Argument[-1];ReturnValue;taint;manual",
"java.io;Reader;true;read;();;Argument[-1];ReturnValue;taint;manual",
// arg to return
Expand All @@ -400,8 +388,6 @@ private class SummaryModelCsvBase extends SummaryModelCsv {
// arg to arg
"java.lang;System;false;arraycopy;;;Argument[0];Argument[2];taint;manual",
// constructor flow
"java.io;File;false;File;;;Argument[0];Argument[-1];taint;manual",
"java.io;File;false;File;;;Argument[1];Argument[-1];taint;manual",
"java.net;URI;false;URI;(String);;Argument[0];Argument[-1];taint;manual",
"java.net;URL;false;URL;(String);;Argument[0];Argument[-1];taint;manual",
"javax.xml.transform.stream;StreamSource;false;StreamSource;;;Argument[0];Argument[-1];taint;manual",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,57 @@ predicate localExprTaint(Expr src, Expr sink) {
localTaint(DataFlow::exprNode(src), DataFlow::exprNode(sink))
}

/** Holds if `node` is an endpoint for local taint flow. */
signature predicate nodeSig(DataFlow::Node node);

/** Provides local taint flow restricted to a given set of sources and sinks. */
module LocalTaintFlow<nodeSig/1 source, nodeSig/1 sink> {
private predicate reachRev(DataFlow::Node n) {
sink(n)
or
exists(DataFlow::Node mid |
localTaintStep(n, mid) and
reachRev(mid)
)
}

private predicate reachFwd(DataFlow::Node n) {
reachRev(n) and
(
source(n)
or
exists(DataFlow::Node mid |
localTaintStep(mid, n) and
reachFwd(mid)
)
)
}

private predicate step(DataFlow::Node n1, DataFlow::Node n2) {
localTaintStep(n1, n2) and
reachFwd(n1) and
reachFwd(n2)
}

/**
* Holds if taint can flow from `n1` to `n2` in zero or more local
* (intra-procedural) steps that are restricted to be part of a path between
* `source` and `sink`.
*/
pragma[inline]
predicate hasFlow(DataFlow::Node n1, DataFlow::Node n2) { step*(n1, n2) }

/**
* Holds if taint can flow from `n1` to `n2` in zero or more local
* (intra-procedural) steps that are restricted to be part of a path between
* `source` and `sink`.
*/
pragma[inline]
predicate hasExprFlow(Expr n1, Expr n2) {
hasFlow(DataFlow::exprNode(n1), DataFlow::exprNode(n2))
}
}

cached
private module Cached {
private import DataFlowImplCommon as DataFlowImplCommon
Expand Down
26 changes: 26 additions & 0 deletions java/ql/lib/semmle/code/java/security/Files.qll
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,29 @@ private class WriteFileSinkModels extends SinkModelCsv {
]
}
}

private class FileSummaryModels extends SummaryModelCsv {
override predicate row(string row) {
row =
[
"java.io;File;false;File;;;Argument[0];Argument[-1];taint;manual",
"java.io;File;false;File;;;Argument[1];Argument[-1];taint;manual",
"java.io;File;true;getAbsoluteFile;;;Argument[-1];ReturnValue;taint;manual",
"java.io;File;true;getAbsolutePath;;;Argument[-1];ReturnValue;taint;manual",
"java.io;File;true;getCanonicalFile;;;Argument[-1];ReturnValue;taint;manual",
"java.io;File;true;getCanonicalPath;;;Argument[-1];ReturnValue;taint;manual",
"java.io;File;true;toPath;;;Argument[-1];ReturnValue;taint;manual",
"java.io;File;true;toString;;;Argument[-1];ReturnValue;taint;manual",
"java.io;File;true;toURI;;;Argument[-1];ReturnValue;taint;manual",
"java.nio.file;Path;true;normalize;;;Argument[-1];ReturnValue;taint;manual",
"java.nio.file;Path;true;resolve;;;Argument[-1..0];ReturnValue;taint;manual",
"java.nio.file;Path;true;toAbsolutePath;;;Argument[-1];ReturnValue;taint;manual",
"java.nio.file;Path;false;toFile;;;Argument[-1];ReturnValue;taint;manual",
"java.nio.file;Path;true;toString;;;Argument[-1];ReturnValue;taint;manual",
"java.nio.file;Path;true;toUri;;;Argument[-1];ReturnValue;taint;manual",
"java.nio.file;Paths;true;get;;;Argument[0..1];ReturnValue;taint;manual",
"java.nio.file;FileSystem;true;getPath;;;Argument[0];ReturnValue;taint;manual",
"java.nio.file;FileSystem;true;getRootDirectories;;;Argument[0];ReturnValue;taint;manual"
]
}
}
277 changes: 277 additions & 0 deletions java/ql/lib/semmle/code/java/security/PathSanitizer.qll
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
/** Provides classes and predicates to reason about sanitization of path injection vulnerabilities. */

import java
private import semmle.code.java.controlflow.Guards
private import semmle.code.java.dataflow.ExternalFlow
private import semmle.code.java.dataflow.FlowSources
private import semmle.code.java.dataflow.SSA

/** A sanitizer that protects against path injection vulnerabilities. */
abstract class PathInjectionSanitizer extends DataFlow::Node { }

/**
* Provides a set of nodes validated by a method that uses a validation guard.
*/
private module ValidationMethod<DataFlow::guardChecksSig/3 validationGuard> {

Check warning

Code scanning / CodeQL

Dead code

Code is dead
/** Gets a node that is safely guarded by a method that uses the given guard check. */
DataFlow::Node getAValidatedNode() {
exists(MethodAccess ma, int pos, RValue rv |
validationMethod(ma.getMethod(), pos) and
ma.getArgument(pos) = rv and
adjacentUseUseSameVar(rv, result.asExpr()) and
ma.getBasicBlock().bbDominates(result.asExpr().getBasicBlock())
)
}

/**
* Holds if `m` validates its `arg`th parameter by using `validationGuard`.
*/
private predicate validationMethod(Method m, int arg) {
exists(
Guard g, SsaImplicitInit var, ControlFlowNode exit, ControlFlowNode normexit, boolean branch
|
validationGuard(g, var.getAUse(), branch) and
var.isParameterDefinition(m.getParameter(arg)) and
exit = m and
normexit.getANormalSuccessor() = exit and
1 = strictcount(ControlFlowNode n | n.getANormalSuccessor() = exit)
|
g.(ConditionNode).getABranchSuccessor(branch) = exit or
g.controls(normexit.getBasicBlock(), branch)
)
}
}

/**
* Holds if `g` is guard that compares a path to a trusted value.
*/
private predicate exactPathMatchGuard(Guard g, Expr e, boolean branch) {
exists(MethodAccess ma, RefType t |
t instanceof TypeString or
t instanceof TypeUri or
t instanceof TypePath or
t instanceof TypeFile or
t.hasQualifiedName("android.net", "Uri")
|
ma.getMethod().getDeclaringType() = t and
ma = g and
ma.getMethod().getName() = ["equals", "equalsIgnoreCase"] and
e = ma.getQualifier() and
branch = true
)
}

private class ExactPathMatchSanitizer extends PathInjectionSanitizer {
ExactPathMatchSanitizer() {
this = DataFlow::BarrierGuard<exactPathMatchGuard/3>::getABarrierNode()
or
this = ValidationMethod<exactPathMatchGuard/3>::getAValidatedNode()
}
}

abstract private class PathGuard extends Guard {
abstract Expr getCheckedExpr();
}

private predicate anyNode(DataFlow::Node n) { any() }

private predicate pathGuardNode(DataFlow::Node n) { n.asExpr() = any(PathGuard g).getCheckedExpr() }

private predicate localTaintFlowToPathGuard(Expr e, PathGuard g) {
TaintTracking::LocalTaintFlow<anyNode/1, pathGuardNode/1>::hasExprFlow(e, g.getCheckedExpr())
}

private class AllowedPrefixGuard extends PathGuard instanceof MethodAccess {
AllowedPrefixGuard() {
(isStringPrefixMatch(this) or isPathPrefixMatch(this)) and
not isDisallowedWord(super.getAnArgument())
}

override Expr getCheckedExpr() { result = super.getQualifier() }
}

/**
* Holds if `g` is a guard that considers a path safe because it is checked against trusted prefixes.
* This requires additional protection against path traversal, either another guard (`PathTraversalGuard`)
* or a sanitizer (`PathNormalizeSanitizer`), to ensure any internal `..` components are removed from the path.
*/
private predicate allowedPrefixGuard(Guard g, Expr e, boolean branch) {
branch = true and
// Local taint-flow is used here to handle cases where the validated expression comes from the
// expression reaching the sink, but it's not the same one, e.g.:
// File file = source();
// String strPath = file.getCanonicalPath();
// if (strPath.startsWith("/safe/dir"))
// sink(file);
g instanceof AllowedPrefixGuard and
localTaintFlowToPathGuard(e, g) and
exists(Expr previousGuard |
localTaintFlowToPathGuard(previousGuard.(PathNormalizeSanitizer), g)
or
previousGuard
.(PathTraversalGuard)
.controls(g.getBasicBlock(), previousGuard.(PathTraversalGuard).getBranch())
)
}

private class AllowedPrefixSanitizer extends PathInjectionSanitizer {
AllowedPrefixSanitizer() {
this = DataFlow::BarrierGuard<allowedPrefixGuard/3>::getABarrierNode() or
this = ValidationMethod<allowedPrefixGuard/3>::getAValidatedNode()
}
}

/**
* Holds if `g` is a guard that considers a path safe because it is checked for `..` components, having previously
* been checked for a trusted prefix.
*/
private predicate dotDotCheckGuard(Guard g, Expr e, boolean branch) {
// Local taint-flow is used here to handle cases where the validated expression comes from the
// expression reaching the sink, but it's not the same one, e.g.:
// Path path = source();
// String strPath = path.toString();
// if (!strPath.contains("..") && strPath.startsWith("/safe/dir"))
// sink(path);
branch = g.(PathTraversalGuard).getBranch() and
localTaintFlowToPathGuard(e, g) and
exists(Guard previousGuard |
previousGuard.(AllowedPrefixGuard).controls(g.getBasicBlock(), true)
or
previousGuard.(BlockListGuard).controls(g.getBasicBlock(), false)
)
}

private class DotDotCheckSanitizer extends PathInjectionSanitizer {
DotDotCheckSanitizer() {
this = DataFlow::BarrierGuard<dotDotCheckGuard/3>::getABarrierNode() or
this = ValidationMethod<dotDotCheckGuard/3>::getAValidatedNode()
}
}

private class BlockListGuard extends PathGuard instanceof MethodAccess {
BlockListGuard() {
(isStringPartialMatch(this) or isPathPrefixMatch(this)) and
isDisallowedWord(super.getAnArgument())
}

override Expr getCheckedExpr() { result = super.getQualifier() }
}

/**
* Holds if `g` is a guard that considers a string safe because it is checked against a blocklist of known dangerous values.
* This requires additional protection against path traversal, either another guard (`PathTraversalGuard`)
* or a sanitizer (`PathNormalizeSanitizer`), to ensure any internal `..` components are removed from the path.
*/
private predicate blockListGuard(Guard g, Expr e, boolean branch) {
branch = false and
// Local taint-flow is used here to handle cases where the validated expression comes from the
// expression reaching the sink, but it's not the same one, e.g.:
// File file = source();
// String strPath = file.getCanonicalPath();
// if (!strPath.contains("..") && !strPath.startsWith("/dangerous/dir"))
// sink(file);
g instanceof BlockListGuard and
localTaintFlowToPathGuard(e, g) and
exists(Expr previousGuard |
localTaintFlowToPathGuard(previousGuard.(PathNormalizeSanitizer), g)
or
previousGuard
.(PathTraversalGuard)
.controls(g.getBasicBlock(), previousGuard.(PathTraversalGuard).getBranch())
)
}

private class BlockListSanitizer extends PathInjectionSanitizer {
BlockListSanitizer() {
this = DataFlow::BarrierGuard<blockListGuard/3>::getABarrierNode() or
this = ValidationMethod<blockListGuard/3>::getAValidatedNode()
}
}

private predicate isStringPrefixMatch(MethodAccess ma) {
exists(Method m | m = ma.getMethod() and m.getDeclaringType() instanceof TypeString |
m.hasName("startsWith")
or
m.hasName("regionMatches") and
ma.getArgument(0).(CompileTimeConstantExpr).getIntValue() = 0
or
m.hasName("matches") and
not ma.getArgument(0).(CompileTimeConstantExpr).getStringValue().matches(".*%")
)
}

/**
* Holds if `ma` is a call to a method that checks a partial string match.
*/
private predicate isStringPartialMatch(MethodAccess ma) {
isStringPrefixMatch(ma)
or
ma.getMethod().getDeclaringType() instanceof TypeString and
ma.getMethod().hasName(["contains", "matches", "regionMatches", "indexOf", "lastIndexOf"])
}

/**
* Holds if `ma` is a call to a method that checks whether a path starts with a prefix.
*/
private predicate isPathPrefixMatch(MethodAccess ma) {
exists(RefType t |
t instanceof TypePath
or
t.hasQualifiedName("kotlin.io", "FilesKt")
|
t = ma.getMethod().getDeclaringType() and
ma.getMethod().hasName("startsWith")
)
}

private predicate isDisallowedWord(CompileTimeConstantExpr word) {
word.getStringValue().matches(["/", "\\", "%WEB-INF%", "%/data%"])
}

/** A complementary guard that protects against path traversal, by looking for the literal `..`. */
private class PathTraversalGuard extends PathGuard {
PathTraversalGuard() {
exists(MethodAccess ma |
ma.getMethod().getDeclaringType() instanceof TypeString and
ma.getAnArgument().(CompileTimeConstantExpr).getStringValue() = ".."
|
this = ma and
ma.getMethod().hasName("contains")
or
exists(EqualityTest eq |
this = eq and
ma.getMethod().hasName(["indexOf", "lastIndexOf"]) and
eq.getAnOperand() = ma and
eq.getAnOperand().(CompileTimeConstantExpr).getIntValue() = -1
)
)
}

override Expr getCheckedExpr() {
exists(MethodAccess ma | ma = this.(EqualityTest).getAnOperand() or ma = this |
result = ma.getQualifier()
)
}

boolean getBranch() {
this instanceof MethodAccess and result = false
or
result = this.(EqualityTest).polarity()
}
}

/** A complementary sanitizer that protects against path traversal using path normalization. */
private class PathNormalizeSanitizer extends MethodAccess {
PathNormalizeSanitizer() {
exists(RefType t |
t instanceof TypePath or
t.hasQualifiedName("kotlin.io", "FilesKt")
|
this.getMethod().getDeclaringType() = t and
this.getMethod().hasName("normalize")
)
or
this.getMethod().getDeclaringType() instanceof TypeFile and
this.getMethod().hasName(["getCanonicalPath", "getCanonicalFile"])
}
}
Loading