Skip to content

Commit 1042c2e

Browse files
Jami CogswellJami Cogswell
authored andcommitted
Java: add taint-tracking config for execute to exclude FPs from non-update queries like select
1 parent 5733011 commit 1042c2e

File tree

4 files changed

+88
-18
lines changed

4 files changed

+88
-18
lines changed

java/ql/lib/semmle/code/java/security/CsrfUnprotectedRequestTypeQuery.qll

Lines changed: 47 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ import java
44
private import semmle.code.java.frameworks.spring.SpringController
55
private import semmle.code.java.frameworks.MyBatis
66
private import semmle.code.java.frameworks.Jdbc
7-
private import semmle.code.java.dataflow.DataFlow
87
private import semmle.code.java.dataflow.ExternalFlow
98
private import semmle.code.java.dispatch.VirtualDispatch
9+
private import semmle.code.java.dataflow.TaintTracking
1010

1111
/** A method that is not protected from CSRF by default. */
1212
abstract class CsrfUnprotectedMethod extends Method { }
@@ -66,24 +66,46 @@ private class PreparedStatementDatabaseUpdateMethod extends DatabaseUpdateMethod
6666
}
6767
}
6868

69-
/** A method that updates a SQL database. */
70-
private class SqlDatabaseUpdateMethod extends DatabaseUpdateMethod {
71-
SqlDatabaseUpdateMethod() {
72-
// TODO: constrain to only insert/update/delete for `execute%` methods; need to track the sql expression into the execute call.
69+
/** A method found via the sql-injection models which may update a SQL database. */
70+
private class SqlInjectionMethod extends DatabaseUpdateMethod {
71+
SqlInjectionMethod() {
7372
exists(DataFlow::Node n | this = n.asExpr().(Argument).getCall().getCallee() |
7473
sinkNode(n, "sql-injection") and
75-
this.getName()
76-
.regexpMatch(".*(?i)(delete|insert|update|save|persist|merge|replicate|execute).*")
74+
this.getName().regexpMatch(".*(?i)(delete|insert|update|save|persist|merge|replicate).*")
75+
or
76+
// do not include `executeQuery` since it is typically used with a select statement
77+
this.hasName("execute")
78+
)
79+
}
80+
}
81+
82+
/**
83+
* A taint-tracking configuration for reasoning about SQL queries that update a database.
84+
*/
85+
module SqlExecuteConfig implements DataFlow::ConfigSig {
86+
predicate isSource(DataFlow::Node source) {
87+
exists(StringLiteral sl | source.asExpr() = sl |
88+
sl.getValue().regexpMatch("^(?i)(insert|update|delete).*")
89+
)
90+
}
91+
92+
predicate isSink(DataFlow::Node sink) {
93+
exists(Method m | m = sink.asExpr().(Argument).getCall().getCallee() |
94+
m instanceof SqlInjectionMethod and
95+
m.hasName("execute")
7796
)
7897
}
7998
}
8099

100+
/** Tracks flow from SQL queries that update a database to the argument of an execute method call. */
101+
module SqlExecuteFlow = TaintTracking::Global<SqlExecuteConfig>;
102+
81103
module CallGraph {
82-
newtype TPathNode =
104+
newtype TCallPathNode =
83105
TMethod(Method m) or
84106
TCall(Call c)
85107

86-
class PathNode extends TPathNode {
108+
class CallPathNode extends TCallPathNode {
87109
Method asMethod() { this = TMethod(result) }
88110

89111
Call asCall() { this = TCall(result) }
@@ -94,16 +116,16 @@ module CallGraph {
94116
result = this.asCall().toString()
95117
}
96118

97-
private PathNode getACallee() {
119+
private CallPathNode getACallee() {
98120
[viableCallable(this.asCall()), this.asCall().getCallee()] = result.asMethod()
99121
}
100122

101-
PathNode getASuccessor() {
123+
CallPathNode getASuccessor() {
102124
this.asMethod() = result.asCall().getEnclosingCallable()
103125
or
104126
result = this.getACallee() and
105127
(
106-
exists(PathNode p |
128+
exists(CallPathNode p |
107129
p = this.getACallee() and
108130
p.asMethod() instanceof DatabaseUpdateMethod
109131
)
@@ -119,15 +141,25 @@ module CallGraph {
119141
}
120142
}
121143

122-
predicate edges(PathNode pred, PathNode succ) { pred.getASuccessor() = succ }
144+
predicate edges(CallPathNode pred, CallPathNode succ) { pred.getASuccessor() = succ }
123145
}
124146

125147
import CallGraph
126148

127149
/** Holds if `src` is an unprotected request handler that reaches a state-changing `sink`. */
128-
predicate unprotectedStateChange(PathNode src, PathNode sink, PathNode sinkPred) {
150+
predicate unprotectedStateChange(CallPathNode src, CallPathNode sink, CallPathNode sinkPred) {
129151
src.asMethod() instanceof CsrfUnprotectedMethod and
130152
sink.asMethod() instanceof DatabaseUpdateMethod and
131153
sinkPred.getASuccessor() = sink and
132-
src.getASuccessor+() = sinkPred
154+
src.getASuccessor+() = sinkPred and
155+
if
156+
sink.asMethod() instanceof SqlInjectionMethod and
157+
sink.asMethod().hasName("execute")
158+
then
159+
exists(SqlExecuteFlow::PathNode executeSrc, SqlExecuteFlow::PathNode executeSink |
160+
SqlExecuteFlow::flowPath(executeSrc, executeSink)
161+
|
162+
sinkPred.asCall() = executeSink.getNode().asExpr().(Argument).getCall()
163+
)
164+
else any()
133165
}

java/ql/src/Security/CWE/CWE-352/CsrfUnprotectedRequestType.ql

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@
1515
import java
1616
import semmle.code.java.security.CsrfUnprotectedRequestTypeQuery
1717

18-
query predicate edges(PathNode pred, PathNode succ) { CallGraph::edges(pred, succ) }
18+
query predicate edges(CallPathNode pred, CallPathNode succ) { CallGraph::edges(pred, succ) }
1919

20-
from PathNode source, PathNode reachable, PathNode callsReachable
20+
from CallPathNode source, CallPathNode reachable, CallPathNode callsReachable
2121
where unprotectedStateChange(source, reachable, callsReachable)
2222
select source.asMethod(), source, callsReachable,
2323
"Potential CSRF vulnerability due to using an HTTP request type which is not default-protected from CSRF for an apparent $@.",

java/ql/test/query-tests/security/CWE-352/CsrfUnprotectedRequestTypeTest.java

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,13 @@
1010
import java.sql.Connection;
1111
import java.sql.PreparedStatement;
1212
import java.sql.SQLException;
13+
import java.sql.Connection;
14+
import java.sql.ResultSet;
15+
import java.sql.Statement;
1316

1417
@Controller
1518
public class CsrfUnprotectedRequestTypeTest {
19+
public static Connection connection;
1620

1721
// Test Spring sources with `PreparedStatement.executeUpdate()` as a default database update method call
1822

@@ -129,6 +133,40 @@ public void bad6() { // $ hasCsrfUnprotectedRequestType
129133
} catch (SQLException e) { }
130134
}
131135

136+
@RequestMapping("/")
137+
public void badStatementExecuteUpdate() { // $ hasCsrfUnprotectedRequestType
138+
try {
139+
String item = "item";
140+
String price = "price";
141+
Statement statement = connection.createStatement();
142+
String query = "UPDATE PRODUCT SET PRICE='" + price + "' WHERE ITEM='" + item + "'";
143+
int count = statement.executeUpdate(query);
144+
} catch (SQLException e) { }
145+
}
146+
147+
@RequestMapping("/")
148+
public void badStatementExecute() { // $ hasCsrfUnprotectedRequestType
149+
try {
150+
String item = "item";
151+
String price = "price";
152+
Statement statement = connection.createStatement();
153+
String query = "UPDATE PRODUCT SET PRICE='" + price + "' WHERE ITEM='" + item + "'";
154+
boolean bool = statement.execute(query);
155+
} catch (SQLException e) { }
156+
}
157+
158+
// GOOD: select not insert/update/delete
159+
@RequestMapping("/")
160+
public void goodStatementExecute() {
161+
try {
162+
String category = "category";
163+
Statement statement = connection.createStatement();
164+
String query = "SELECT ITEM,PRICE FROM PRODUCT WHERE ITEM_CATEGORY='"
165+
+ category + "' ORDER BY PRICE";
166+
boolean bool = statement.execute(query);
167+
} catch (SQLException e) { }
168+
}
169+
132170
@Autowired
133171
private MyBatisService myBatisService;
134172

java/ql/test/query-tests/security/CWE-352/CsrfUnprotectedRequestTypeTest.ql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ module CsrfUnprotectedRequestTypeTest implements TestSig {
77

88
predicate hasActualResult(Location location, string element, string tag, string value) {
99
tag = "hasCsrfUnprotectedRequestType" and
10-
exists(PathNode src, PathNode sink, PathNode sinkPred |
10+
exists(CallPathNode src, CallPathNode sink, CallPathNode sinkPred |
1111
unprotectedStateChange(src, sink, sinkPred)
1212
|
1313
src.getLocation() = location and

0 commit comments

Comments
 (0)