Skip to content

Commit 0f39011

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 97aaf4c commit 0f39011

File tree

4 files changed

+84
-16
lines changed

4 files changed

+84
-16
lines changed

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

Lines changed: 43 additions & 13 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,10 +66,9 @@ 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
7574
// do not include `executeQuery` since it is typically used with a select statement
@@ -81,12 +80,33 @@ private class SqlDatabaseUpdateMethod extends DatabaseUpdateMethod {
8180
}
8281
}
8382

83+
/**
84+
* A taint-tracking configuration for reasoning about SQL queries that update a database.
85+
*/
86+
module SqlExecuteConfig implements DataFlow::ConfigSig {
87+
predicate isSource(DataFlow::Node source) {
88+
exists(StringLiteral sl | source.asExpr() = sl |
89+
sl.getValue().regexpMatch("^(?i)(insert|update|delete).*")
90+
)
91+
}
92+
93+
predicate isSink(DataFlow::Node sink) {
94+
exists(Method m | m = sink.asExpr().(Argument).getCall().getCallee() |
95+
m instanceof SqlInjectionMethod and
96+
m.hasName("execute")
97+
)
98+
}
99+
}
100+
101+
/** Tracks flow from SQL queries that update a database to the argument of an execute method call. */
102+
module SqlExecuteFlow = TaintTracking::Global<SqlExecuteConfig>;
103+
84104
module CallGraph {
85-
newtype TPathNode =
105+
newtype TCallPathNode =
86106
TMethod(Method m) or
87107
TCall(Call c)
88108

89-
class PathNode extends TPathNode {
109+
class CallPathNode extends TCallPathNode {
90110
Method asMethod() { this = TMethod(result) }
91111

92112
Call asCall() { this = TCall(result) }
@@ -97,16 +117,16 @@ module CallGraph {
97117
result = this.asCall().toString()
98118
}
99119

100-
private PathNode getACallee() {
120+
private CallPathNode getACallee() {
101121
[viableCallable(this.asCall()), this.asCall().getCallee()] = result.asMethod()
102122
}
103123

104-
PathNode getASuccessor() {
124+
CallPathNode getASuccessor() {
105125
this.asMethod() = result.asCall().getEnclosingCallable()
106126
or
107127
result = this.getACallee() and
108128
(
109-
exists(PathNode p |
129+
exists(CallPathNode p |
110130
p = this.getACallee() and
111131
p.asMethod() instanceof DatabaseUpdateMethod
112132
)
@@ -122,15 +142,25 @@ module CallGraph {
122142
}
123143
}
124144

125-
predicate edges(PathNode pred, PathNode succ) { pred.getASuccessor() = succ }
145+
predicate edges(CallPathNode pred, CallPathNode succ) { pred.getASuccessor() = succ }
126146
}
127147

128148
import CallGraph
129149

130150
/** Holds if `src` is an unprotected request handler that reaches a state-changing `sink`. */
131-
predicate unprotectedStateChange(PathNode src, PathNode sink, PathNode sinkPred) {
151+
predicate unprotectedStateChange(CallPathNode src, CallPathNode sink, CallPathNode sinkPred) {
132152
src.asMethod() instanceof CsrfUnprotectedMethod and
133153
sink.asMethod() instanceof DatabaseUpdateMethod and
134154
sinkPred.getASuccessor() = sink and
135-
src.getASuccessor+() = sinkPred
155+
src.getASuccessor+() = sinkPred and
156+
if
157+
sink.asMethod() instanceof SqlInjectionMethod and
158+
sink.asMethod().hasName("execute")
159+
then
160+
exists(SqlExecuteFlow::PathNode executeSrc, SqlExecuteFlow::PathNode executeSink |
161+
SqlExecuteFlow::flowPath(executeSrc, executeSink)
162+
|
163+
sinkPred.asCall() = executeSink.getNode().asExpr().(Argument).getCall()
164+
)
165+
else any()
136166
}

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)