Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rust: SQL Injection Query #18025

Merged
merged 14 commits into from
Nov 21, 2024
Merged
Show file tree
Hide file tree
Changes from 7 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
136 changes: 136 additions & 0 deletions rust/ql/lib/codeql/rust/Concepts.qll
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/**
* Provides abstract classes representing generic concepts such as file system
* access or system command execution, for which individual framework libraries
* provide concrete subclasses.
*/

private import codeql.rust.dataflow.DataFlow
private import codeql.threatmodels.ThreatModels

/**
* A data flow source for a specific threat-model.
*
* Extend this class to refine existing API models. If you want to model new APIs,
* extend `ThreatModelSource::Range` instead.
*/
class ThreatModelSource extends DataFlow::Node instanceof ThreatModelSource::Range {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't this simply be final class ThreatModelSource = ThreatModelSource::Range?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't worked with Range classes before. My understanding is that in the frameworks we're expected to sometimes want to override both, so this can't be final, but .... I will admit I'm not really sure what the point of a separate class and Range is.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see why one would want to override both ThreatModelSource and ThreatModelSource::Range. I think we should change it like above.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I think I see what's going on here. The intent of the "Range" pattern is that you can extend the abstract Range class to expand the class to cover new things (i.e. in this case to add new sources). But if you want to define something in terms of ThreatModelSource (i.e. extend it the non-abstract way, as ActiveThreatModelSource does), you use the non-Range version. Making it final actually doesn't remove the latter possibility, thanks to "final extensions" (which I haven't knowingly used much before).

Change made. The code is quite a lot cleaner as a result. Other languages could implement the same change in their Concepts.qll.

/**
* Gets a string that represents the source kind with respect to threat modeling.
*
* See
* - https://github.com/github/codeql/blob/main/docs/codeql/reusables/threat-model-description.rst
* - https://github.com/github/codeql/blob/main/shared/threat-models/ext/threat-model-grouping.model.yml
*/
string getThreatModel() { result = super.getThreatModel() }

/**
* Gets a string that describes the type of this threat-model source.
*/
string getSourceType() { result = super.getSourceType() }
}

/**
* Provides a class for modeling new sources for specific threat-models.
*/
module ThreatModelSource {
/**
* A data flow source, for a specific threat-model.
*/
abstract class Range extends DataFlow::Node {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd be happy to do away with the doc on the ThreadModelSource module and ThreadModelSource::Range class, as it really just duplicates that on ThreadModelSource. I don't think CI would accept that though???

/**
* Gets a string that represents the source kind with respect to threat modeling.
*/
abstract string getThreatModel();

/**
* Gets a string that describes the type of this threat-model source.
*/
abstract string getSourceType();
}
}

/**
* A data flow source that is enabled in the current threat model configuration.
*/
class ActiveThreatModelSource extends ThreatModelSource {
ActiveThreatModelSource() { currentThreatModel(this.getThreatModel()) }
}

/**
* A data-flow node that constructs a SQL statement.
*
* Often, it is worthy of an alert if a SQL statement is constructed such that
* executing it would be a security risk.
*
* If it is important that the SQL statement is executed, use `SqlExecution`.
*
* Extend this class to refine existing API models. If you want to model new APIs,
* extend `SqlConstruction::Range` instead.
*/
class SqlConstruction extends DataFlow::Node instanceof SqlConstruction::Range {
geoffw0 marked this conversation as resolved.
Show resolved Hide resolved
/**
* Gets the argument that specifies the SQL statements to be constructed.
*/
DataFlow::Node getSql() { result = super.getSql() }
}

/**
* Provides a class for modeling new SQL execution APIs.
*/
module SqlConstruction {
/**
* A data-flow node that constructs a SQL statement.
*/
abstract class Range extends DataFlow::Node {
/**
* Gets the argument that specifies the SQL statements to be constructed.
*/
abstract DataFlow::Node getSql();
}
}

/**
* A data-flow node that executes SQL statements.
*
* If the context of interest is such that merely constructing a SQL statement
* would be valuable to report, consider using `SqlConstruction`.
*
* Extend this class to refine existing API models. If you want to model new APIs,
* extend `SqlExecution::Range` instead.
*/
class SqlExecution extends DataFlow::Node instanceof SqlExecution::Range {
geoffw0 marked this conversation as resolved.
Show resolved Hide resolved
/**
* Gets the argument that specifies the SQL statements to be executed.
*/
DataFlow::Node getSql() { result = super.getSql() }
}

/**
* Provides a class for modeling new SQL execution APIs.
*/
module SqlExecution {
/**
* A data-flow node that executes SQL statements.
*/
abstract class Range extends DataFlow::Node {
/**
* Gets the argument that specifies the SQL statements to be executed.
*/
abstract DataFlow::Node getSql();
}
}

/**
* A data-flow node that performs SQL sanitization.
*/
class SqlSanitization extends DataFlow::Node instanceof SqlSanitization::Range { }
geoffw0 marked this conversation as resolved.
Show resolved Hide resolved

/**
* Provides a class for modeling new SQL sanitization APIs.
*/
module SqlSanitization {
/**
* A data-flow node that performs SQL sanitization.
*/
abstract class Range extends DataFlow::Node { }
}
50 changes: 50 additions & 0 deletions rust/ql/lib/codeql/rust/security/SqlInjectionExtensions.qll
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/**
* Provides classes and predicates for reasoning about database
* queries built from user-controlled sources (that is, SQL injection
* vulnerabilities).
*/

import rust
private import codeql.rust.dataflow.DataFlow
private import codeql.rust.Concepts
private import codeql.util.Unit

/**
* Provides default sources, sinks and barriers for detecting SQL injection
* vulnerabilities, as well as extension points for adding your own.
*/
module SqlInjection {
/**
* A data flow source for SQL injection vulnerabilities.
*/
abstract class Source extends DataFlow::Node { }

/**
* A data flow sink for SQL injection vulnerabilities.
*/
abstract class Sink extends DataFlow::Node { }

/**
* A barrier for SQL injection vulnerabilities.
*/
abstract class Barrier extends DataFlow::Node { }

/**
* An active threat-model source, considered as a flow source.
*/
private class ActiveThreatModelSourceAsSource extends Source, ActiveThreatModelSource { }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be restricted somehow using getSourceType()?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so. ActiveThreatModelSource is a restriction of ThreatModelSource according to user preferences (however that works), which defaults to just the remote sources I think. This sounds like exactly what we want.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah ok, to ActiveThreatModelSource should be used for all queries that already use remote flow sources?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe that's the idea. Unless you have a special reason for really only wanting remote flow sources, but generally in the past we've generally specified remote only because most users aren't interested in flow from argv


/**
* A flow sink that is the statement of an SQL construction.
*/
class SqlConstructionAsSink extends Sink {
SqlConstructionAsSink() { this = any(SqlConstruction c).getSql() }
}

/**
* A flow sink that is the statement of an SQL execution.
*/
class SqlExecutionAsSink extends Sink {
SqlExecutionAsSink() { this = any(SqlExecution e).getSql() }
}
}
1 change: 1 addition & 0 deletions rust/ql/lib/qlpack.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ dependencies:
codeql/controlflow: ${workspace}
codeql/dataflow: ${workspace}
codeql/regex: ${workspace}
codeql/threat-models: ${workspace}
codeql/mad: ${workspace}
codeql/ssa: ${workspace}
codeql/tutorial: ${workspace}
Expand Down
39 changes: 39 additions & 0 deletions rust/ql/src/queries/security/CWE-089/SqlInjection.qhelp
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<!DOCTYPE qhelp PUBLIC
"-//Semmle//qhelp//EN"
"qhelp.dtd">
<qhelp>
<overview>

<p>
If a database query (such as a 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.
geoffw0 marked this conversation as resolved.
Show resolved Hide resolved
mchammer01 marked this conversation as resolved.
Show resolved Hide resolved
</p>

</overview>
<recommendation>

<p>
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.
</p>

</recommendation>
<example>

<p>
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.
</p>

<sample src="SqlInjectionBad.rs" />

<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.
</p>

<sample src="SqlInjectionGood.rs" />

</example>
<references>

<li>Wikipedia: <a href="https://en.wikipedia.org/wiki/SQL_injection">SQL injection</a>.</li>
<li>OWASP: <a href="https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html">SQL Injection Prevention Cheat Sheet</a>.</li>

</references>
</qhelp>
38 changes: 38 additions & 0 deletions rust/ql/src/queries/security/CWE-089/SqlInjection.ql
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* @name Database query built from user-controlled sources
* @description Building a database query from user-controlled sources is vulnerable to insertion of malicious code by the user.
geoffw0 marked this conversation as resolved.
Show resolved Hide resolved
* @kind path-problem
* @problem.severity error
* @security-severity 8.8
* @precision high
* @id rust/sql-injection
* @tags security
* external/cwe/cwe-089
*/

import rust
import codeql.rust.dataflow.DataFlow
import codeql.rust.dataflow.TaintTracking
import codeql.rust.security.SqlInjectionExtensions
import SqlInjectionFlow::PathGraph

/**
* A taint configuration for tainted data that reaches a SQL sink.
*/
Comment on lines +19 to +21
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this comment is redundant.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As in, it doesn't say much that isn't obvious from the code below it?

I tend to comment nearly every file, class and module (with occasional exceptions). One reason is I think it's a requirement in .qlls. Another is that I personally tend to read comments before I look at code, so a module without a comment is harder to read...

module SqlInjectionConfig implements DataFlow::ConfigSig {
predicate isSource(DataFlow::Node node) { node instanceof SqlInjection::Source }

predicate isSink(DataFlow::Node node) { node instanceof SqlInjection::Sink }

predicate isBarrier(DataFlow::Node barrier) { barrier instanceof SqlInjection::Barrier }
}

/**
* Detect taint flow of tainted data that reaches a SQL sink.
geoffw0 marked this conversation as resolved.
Show resolved Hide resolved
*/
module SqlInjectionFlow = TaintTracking::Global<SqlInjectionConfig>;

from SqlInjectionFlow::PathNode sourceNode, SqlInjectionFlow::PathNode sinkNode
where SqlInjectionFlow::flowPath(sourceNode, sinkNode)
select sinkNode.getNode(), sourceNode, sinkNode, "This query depends on a $@.",
sourceNode.getNode(), "user-provided value"
geoffw0 marked this conversation as resolved.
Show resolved Hide resolved
7 changes: 7 additions & 0 deletions rust/ql/src/queries/security/CWE-089/SqlInjectionBad.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// with SQLx

let unsafe_query = format!("SELECT * FROM people WHERE firstname='{remote_controlled_string}'");

let _ = conn.execute(unsafe_query.as_str()).await?; // BAD (arbitrary SQL injection is possible)

let _ = sqlx::query(unsafe_query.as_str()).fetch_all(&mut conn).await?; // $ BAD (arbitrary SQL injection is possible)
geoffw0 marked this conversation as resolved.
Show resolved Hide resolved
5 changes: 5 additions & 0 deletions rust/ql/src/queries/security/CWE-089/SqlInjectionGood.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// with SQLx

let prepared_query = "SELECT * FROM people WHERE firstname=?";

let _ = sqlx::query(prepared_query_1).bind(&remote_controlled_string).fetch_all(&mut conn).await?; // GOOD (prepared statement with bound parameter)
2 changes: 2 additions & 0 deletions rust/ql/test/query-tests/security/CWE-089/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# sqlite database
*.db*

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
uniqueNodeToString
| sqlx.rs:154:13:154:81 | (no string representation) | Node should have one toString but has 0. |
| sqlx.rs:156:17:156:86 | (no string representation) | Node should have one toString but has 0. |
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#select
edges
nodes
subpaths
2 changes: 2 additions & 0 deletions rust/ql/test/query-tests/security/CWE-089/SqlInjection.qlref
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
query: queries/security/CWE-089/SqlInjection.ql
postprocess: utils/InlineExpectationsTestQuery.ql
15 changes: 15 additions & 0 deletions rust/ql/test/query-tests/security/CWE-089/cargo.toml.manual
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[workspace]

[package]
name = "CWE-089-Test"
version = "0.1.0"
edition = "2021"

[dependencies]
reqwest = { version = "0.12.9", features = ["blocking"] }
sqlx = { version = "0.8", features = ["mysql", "sqlite", "postgres", "runtime-async-std", "tls-native-tls"] }
futures = { version = "0.3" }

[[bin]]
name = "sqlx"
path = "./sqlx.rs"
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
CREATE TABLE IF NOT EXISTS people
(
id INTEGER PRIMARY KEY NOT NULL,
firstname TEXT NOT NULL,
lastname TEXT NOT NULL
);

INSERT INTO people
VALUES (1, "Alice", "Adams");

INSERT INTO people
VALUES (2, "Bob", "Becket");
5 changes: 5 additions & 0 deletions rust/ql/test/query-tests/security/CWE-089/options.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
qltest_cargo_check: true
qltest_dependencies:
- reqwest = { version = "0.12.9", features = ["blocking"] }
- sqlx = { version = "0.8", features = ["mysql", "sqlite", "postgres", "runtime-async-std", "tls-native-tls"] }
- futures = { version = "0.3" }
Loading