Skip to content

Commit b6cdae2

Browse files
authored
Merge pull request #18025 from geoffw0/sql1
Rust: SQL Injection Query
2 parents d3dd944 + 01cddcc commit b6cdae2

16 files changed

+550
-0
lines changed

rust/ql/lib/codeql/rust/Concepts.qll

+116
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
/**
2+
* Provides abstract classes representing generic concepts such as file system
3+
* access or system command execution, for which individual framework libraries
4+
* provide concrete subclasses.
5+
*/
6+
7+
private import codeql.rust.dataflow.DataFlow
8+
private import codeql.threatmodels.ThreatModels
9+
10+
/**
11+
* A data flow source for a specific threat-model.
12+
*
13+
* Extend this class to refine existing API models. If you want to model new APIs,
14+
* extend `ThreatModelSource::Range` instead.
15+
*/
16+
final class ThreatModelSource = ThreatModelSource::Range;
17+
18+
/**
19+
* Provides a class for modeling new sources for specific threat-models.
20+
*/
21+
module ThreatModelSource {
22+
/**
23+
* A data flow source, for a specific threat-model.
24+
*/
25+
abstract class Range extends DataFlow::Node {
26+
/**
27+
* Gets a string that represents the source kind with respect to threat modeling.
28+
*
29+
* See
30+
* - https://github.com/github/codeql/blob/main/docs/codeql/reusables/threat-model-description.rst
31+
* - https://github.com/github/codeql/blob/main/shared/threat-models/ext/threat-model-grouping.model.yml
32+
*/
33+
abstract string getThreatModel();
34+
35+
/**
36+
* Gets a string that describes the type of this threat-model source.
37+
*/
38+
abstract string getSourceType();
39+
}
40+
}
41+
42+
/**
43+
* A data flow source that is enabled in the current threat model configuration.
44+
*/
45+
class ActiveThreatModelSource extends ThreatModelSource {
46+
ActiveThreatModelSource() { currentThreatModel(this.getThreatModel()) }
47+
}
48+
49+
/**
50+
* A data-flow node that constructs a SQL statement.
51+
*
52+
* Often, it is worthy of an alert if a SQL statement is constructed such that
53+
* executing it would be a security risk.
54+
*
55+
* If it is important that the SQL statement is executed, use `SqlExecution`.
56+
*
57+
* Extend this class to refine existing API models. If you want to model new APIs,
58+
* extend `SqlConstruction::Range` instead.
59+
*/
60+
final class SqlConstruction = SqlConstruction::Range;
61+
62+
/**
63+
* Provides a class for modeling new SQL execution APIs.
64+
*/
65+
module SqlConstruction {
66+
/**
67+
* A data-flow node that constructs a SQL statement.
68+
*/
69+
abstract class Range extends DataFlow::Node {
70+
/**
71+
* Gets the argument that specifies the SQL statements to be constructed.
72+
*/
73+
abstract DataFlow::Node getSql();
74+
}
75+
}
76+
77+
/**
78+
* A data-flow node that executes SQL statements.
79+
*
80+
* If the context of interest is such that merely constructing a SQL statement
81+
* would be valuable to report, consider using `SqlConstruction`.
82+
*
83+
* Extend this class to refine existing API models. If you want to model new APIs,
84+
* extend `SqlExecution::Range` instead.
85+
*/
86+
final class SqlExecution = SqlExecution::Range;
87+
88+
/**
89+
* Provides a class for modeling new SQL execution APIs.
90+
*/
91+
module SqlExecution {
92+
/**
93+
* A data-flow node that executes SQL statements.
94+
*/
95+
abstract class Range extends DataFlow::Node {
96+
/**
97+
* Gets the argument that specifies the SQL statements to be executed.
98+
*/
99+
abstract DataFlow::Node getSql();
100+
}
101+
}
102+
103+
/**
104+
* A data-flow node that performs SQL sanitization.
105+
*/
106+
final class SqlSanitization = SqlSanitization::Range;
107+
108+
/**
109+
* Provides a class for modeling new SQL sanitization APIs.
110+
*/
111+
module SqlSanitization {
112+
/**
113+
* A data-flow node that performs SQL sanitization.
114+
*/
115+
abstract class Range extends DataFlow::Node { }
116+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/**
2+
* Provides classes and predicates for reasoning about database
3+
* queries built from user-controlled sources (that is, SQL injection
4+
* vulnerabilities).
5+
*/
6+
7+
import rust
8+
private import codeql.rust.dataflow.DataFlow
9+
private import codeql.rust.Concepts
10+
private import codeql.util.Unit
11+
12+
/**
13+
* Provides default sources, sinks and barriers for detecting SQL injection
14+
* vulnerabilities, as well as extension points for adding your own.
15+
*/
16+
module SqlInjection {
17+
/**
18+
* A data flow source for SQL injection vulnerabilities.
19+
*/
20+
abstract class Source extends DataFlow::Node { }
21+
22+
/**
23+
* A data flow sink for SQL injection vulnerabilities.
24+
*/
25+
abstract class Sink extends DataFlow::Node { }
26+
27+
/**
28+
* A barrier for SQL injection vulnerabilities.
29+
*/
30+
abstract class Barrier extends DataFlow::Node { }
31+
32+
/**
33+
* An active threat-model source, considered as a flow source.
34+
*/
35+
private class ActiveThreatModelSourceAsSource extends Source, ActiveThreatModelSource { }
36+
37+
/**
38+
* A flow sink that is the statement of an SQL construction.
39+
*/
40+
class SqlConstructionAsSink extends Sink {
41+
SqlConstructionAsSink() { this = any(SqlConstruction c).getSql() }
42+
}
43+
44+
/**
45+
* A flow sink that is the statement of an SQL execution.
46+
*/
47+
class SqlExecutionAsSink extends Sink {
48+
SqlExecutionAsSink() { this = any(SqlExecution e).getSql() }
49+
}
50+
}

rust/ql/lib/qlpack.yml

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ dependencies:
88
codeql/controlflow: ${workspace}
99
codeql/dataflow: ${workspace}
1010
codeql/regex: ${workspace}
11+
codeql/threat-models: ${workspace}
1112
codeql/mad: ${workspace}
1213
codeql/ssa: ${workspace}
1314
codeql/tutorial: ${workspace}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<!DOCTYPE qhelp PUBLIC
2+
"-//Semmle//qhelp//EN"
3+
"qhelp.dtd">
4+
<qhelp>
5+
<overview>
6+
7+
<p>
8+
If a database query (such as an SQL query) is built from user-provided data without sufficient sanitization, a user may be able to run malicious database queries. An attacker can craft the part of the query they control to change the overall meaning of the query.
9+
</p>
10+
11+
</overview>
12+
<recommendation>
13+
14+
<p>
15+
Most database connector libraries offer a way to safely embed untrusted data into a query using query parameters or prepared statements. You should use these features to build queries, rather than string concatenation or similar methods. You can also escape (sanitize) user-controlled strings so that they can be included directly in an SQL command. A library function should be used for escaping, because this approach is only safe if the escaping function is robust against all possible inputs.
16+
</p>
17+
18+
</recommendation>
19+
<example>
20+
21+
<p>
22+
In the following examples, an SQL query is prepared using string formatting to directly include a user-controlled value <code>remote_controlled_string</code>. An attacker could craft <code>remote_controlled_string</code> to change the overall meaning of the SQL query.
23+
</p>
24+
25+
<sample src="SqlInjectionBad.rs" />
26+
27+
<p>A better way to do this is with a prepared statement, binding <code>remote_controlled_string</code> to a parameter of that statement. An attacker who controls <code>remote_controlled_string</code> now cannot change the overall meaning of the query.
28+
</p>
29+
30+
<sample src="SqlInjectionGood.rs" />
31+
32+
</example>
33+
<references>
34+
35+
<li>Wikipedia: <a href="https://en.wikipedia.org/wiki/SQL_injection">SQL injection</a>.</li>
36+
<li>OWASP: <a href="https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html">SQL Injection Prevention Cheat Sheet</a>.</li>
37+
38+
</references>
39+
</qhelp>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/**
2+
* @name Database query built from user-controlled sources
3+
* @description Building a database query from user-controlled sources is vulnerable to insertion of malicious code by attackers.
4+
* @kind path-problem
5+
* @problem.severity error
6+
* @security-severity 8.8
7+
* @precision high
8+
* @id rust/sql-injection
9+
* @tags security
10+
* external/cwe/cwe-089
11+
*/
12+
13+
import rust
14+
import codeql.rust.dataflow.DataFlow
15+
import codeql.rust.dataflow.TaintTracking
16+
import codeql.rust.security.SqlInjectionExtensions
17+
import SqlInjectionFlow::PathGraph
18+
19+
/**
20+
* A taint configuration for tainted data that reaches a SQL sink.
21+
*/
22+
module SqlInjectionConfig implements DataFlow::ConfigSig {
23+
predicate isSource(DataFlow::Node node) { node instanceof SqlInjection::Source }
24+
25+
predicate isSink(DataFlow::Node node) { node instanceof SqlInjection::Sink }
26+
27+
predicate isBarrier(DataFlow::Node barrier) { barrier instanceof SqlInjection::Barrier }
28+
}
29+
30+
module SqlInjectionFlow = TaintTracking::Global<SqlInjectionConfig>;
31+
32+
from SqlInjectionFlow::PathNode sourceNode, SqlInjectionFlow::PathNode sinkNode
33+
where SqlInjectionFlow::flowPath(sourceNode, sinkNode)
34+
select sinkNode.getNode(), sourceNode, sinkNode, "This query depends on a $@.",
35+
sourceNode.getNode(), "user-provided value"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// with SQLx
2+
3+
let unsafe_query = format!("SELECT * FROM people WHERE firstname='{remote_controlled_string}'");
4+
5+
let _ = conn.execute(unsafe_query.as_str()).await?; // BAD (arbitrary SQL injection is possible)
6+
7+
let _ = sqlx::query(unsafe_query.as_str()).fetch_all(&mut conn).await?; // BAD (arbitrary SQL injection is possible)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
// with SQLx
2+
3+
let prepared_query = "SELECT * FROM people WHERE firstname=?";
4+
5+
let _ = sqlx::query(prepared_query_1).bind(&remote_controlled_string).fetch_all(&mut conn).await?; // GOOD (prepared statement with bound parameter)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# sqlite database
2+
*.db*

rust/ql/test/query-tests/security/CWE-089/.sqlx/query-c996a36820ff0b98021fa553b09b6da5ed65c28f666a68c4d73a1918f0eaa6f6.json

+32
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
uniqueEnclosingCallable
2+
| sqlx.rs:52:72:52:84 | remote_number | Node should have one enclosing callable but has 0. |
3+
| sqlx.rs:56:74:56:86 | remote_string | Node should have one enclosing callable but has 0. |
4+
| sqlx.rs:199:32:199:44 | enable_remote | Node should have one enclosing callable but has 0. |
5+
uniqueNodeToString
6+
| sqlx.rs:154:13:154:81 | (no string representation) | Node should have one toString but has 0. |
7+
| sqlx.rs:156:17:156:86 | (no string representation) | Node should have one toString but has 0. |
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
#select
2+
edges
3+
nodes
4+
subpaths
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
query: queries/security/CWE-089/SqlInjection.ql
2+
postprocess: utils/InlineExpectationsTestQuery.ql
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
[workspace]
2+
3+
[package]
4+
name = "CWE-089-Test"
5+
version = "0.1.0"
6+
edition = "2021"
7+
8+
[dependencies]
9+
reqwest = { version = "0.12.9", features = ["blocking"] }
10+
sqlx = { version = "0.8", features = ["mysql", "sqlite", "postgres", "runtime-async-std", "tls-native-tls"] }
11+
futures = { version = "0.3" }
12+
13+
[[bin]]
14+
name = "sqlx"
15+
path = "./sqlx.rs"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
CREATE TABLE IF NOT EXISTS people
2+
(
3+
id INTEGER PRIMARY KEY NOT NULL,
4+
firstname TEXT NOT NULL,
5+
lastname TEXT NOT NULL
6+
);
7+
8+
INSERT INTO people
9+
VALUES (1, "Alice", "Adams");
10+
11+
INSERT INTO people
12+
VALUES (2, "Bob", "Becket");
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
qltest_cargo_check: true
2+
qltest_dependencies:
3+
- reqwest = { version = "0.12.9", features = ["blocking"] }
4+
- sqlx = { version = "0.8", features = ["mysql", "sqlite", "postgres", "runtime-async-std", "tls-native-tls"] }
5+
- futures = { version = "0.3" }

0 commit comments

Comments
 (0)