diff --git a/rust/ql/lib/codeql/rust/Concepts.qll b/rust/ql/lib/codeql/rust/Concepts.qll new file mode 100644 index 000000000000..5befe006bc69 --- /dev/null +++ b/rust/ql/lib/codeql/rust/Concepts.qll @@ -0,0 +1,116 @@ +/** + * 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. + */ +final class ThreatModelSource = ThreatModelSource::Range; + +/** + * 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 { + /** + * 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 + */ + 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. + */ +final class SqlConstruction = SqlConstruction::Range; + +/** + * 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. + */ +final class SqlExecution = SqlExecution::Range; + +/** + * 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. + */ +final class SqlSanitization = SqlSanitization::Range; + +/** + * 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 { } +} diff --git a/rust/ql/lib/codeql/rust/security/SqlInjectionExtensions.qll b/rust/ql/lib/codeql/rust/security/SqlInjectionExtensions.qll new file mode 100644 index 000000000000..db6239d78110 --- /dev/null +++ b/rust/ql/lib/codeql/rust/security/SqlInjectionExtensions.qll @@ -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 { } + + /** + * 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() } + } +} diff --git a/rust/ql/lib/qlpack.yml b/rust/ql/lib/qlpack.yml index 649d02c8e3aa..53ccf6dfced4 100644 --- a/rust/ql/lib/qlpack.yml +++ b/rust/ql/lib/qlpack.yml @@ -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} diff --git a/rust/ql/src/queries/security/CWE-089/SqlInjection.qhelp b/rust/ql/src/queries/security/CWE-089/SqlInjection.qhelp new file mode 100644 index 000000000000..0b56ac54d0de --- /dev/null +++ b/rust/ql/src/queries/security/CWE-089/SqlInjection.qhelp @@ -0,0 +1,39 @@ + + + + +

+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. +

+ +
+ + +

+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. +

+ +
+ + +

+In the following examples, an SQL query is prepared using string formatting to directly include a user-controlled value remote_controlled_string. An attacker could craft remote_controlled_string to change the overall meaning of the SQL query. +

+ + + +

A better way to do this is with a prepared statement, binding remote_controlled_string to a parameter of that statement. An attacker who controls remote_controlled_string now cannot change the overall meaning of the query. +

+ + + +
+ + +
  • Wikipedia: SQL injection.
  • +
  • OWASP: SQL Injection Prevention Cheat Sheet.
  • + +
    +
    diff --git a/rust/ql/src/queries/security/CWE-089/SqlInjection.ql b/rust/ql/src/queries/security/CWE-089/SqlInjection.ql new file mode 100644 index 000000000000..ee2a3d144868 --- /dev/null +++ b/rust/ql/src/queries/security/CWE-089/SqlInjection.ql @@ -0,0 +1,35 @@ +/** + * @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 attackers. + * @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. + */ +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 } +} + +module SqlInjectionFlow = TaintTracking::Global; + +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" diff --git a/rust/ql/src/queries/security/CWE-089/SqlInjectionBad.rs b/rust/ql/src/queries/security/CWE-089/SqlInjectionBad.rs new file mode 100644 index 000000000000..4184399a4f18 --- /dev/null +++ b/rust/ql/src/queries/security/CWE-089/SqlInjectionBad.rs @@ -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) diff --git a/rust/ql/src/queries/security/CWE-089/SqlInjectionGood.rs b/rust/ql/src/queries/security/CWE-089/SqlInjectionGood.rs new file mode 100644 index 000000000000..238665cc2739 --- /dev/null +++ b/rust/ql/src/queries/security/CWE-089/SqlInjectionGood.rs @@ -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) diff --git a/rust/ql/test/query-tests/security/CWE-089/.gitignore b/rust/ql/test/query-tests/security/CWE-089/.gitignore new file mode 100644 index 000000000000..bdcfa82b5d5c --- /dev/null +++ b/rust/ql/test/query-tests/security/CWE-089/.gitignore @@ -0,0 +1,2 @@ +# sqlite database +*.db* diff --git a/rust/ql/test/query-tests/security/CWE-089/.sqlx/query-c996a36820ff0b98021fa553b09b6da5ed65c28f666a68c4d73a1918f0eaa6f6.json b/rust/ql/test/query-tests/security/CWE-089/.sqlx/query-c996a36820ff0b98021fa553b09b6da5ed65c28f666a68c4d73a1918f0eaa6f6.json new file mode 100644 index 000000000000..a4493e90c37d --- /dev/null +++ b/rust/ql/test/query-tests/security/CWE-089/.sqlx/query-c996a36820ff0b98021fa553b09b6da5ed65c28f666a68c4d73a1918f0eaa6f6.json @@ -0,0 +1,32 @@ +{ + "db_name": "SQLite", + "query": "SELECT * FROM people WHERE firstname=$1", + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Integer" + }, + { + "name": "firstname", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "lastname", + "ordinal": 2, + "type_info": "Text" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false + ] + }, + "hash": "c996a36820ff0b98021fa553b09b6da5ed65c28f666a68c4d73a1918f0eaa6f6" +} diff --git a/rust/ql/test/query-tests/security/CWE-089/CONSISTENCY/DataFlowConsistency.expected b/rust/ql/test/query-tests/security/CWE-089/CONSISTENCY/DataFlowConsistency.expected new file mode 100644 index 000000000000..6e35019c1cbf --- /dev/null +++ b/rust/ql/test/query-tests/security/CWE-089/CONSISTENCY/DataFlowConsistency.expected @@ -0,0 +1,7 @@ +uniqueEnclosingCallable +| sqlx.rs:52:72:52:84 | remote_number | Node should have one enclosing callable but has 0. | +| sqlx.rs:56:74:56:86 | remote_string | Node should have one enclosing callable but has 0. | +| sqlx.rs:199:32:199:44 | enable_remote | Node should have one enclosing callable but has 0. | +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. | diff --git a/rust/ql/test/query-tests/security/CWE-089/SqlInjection.expected b/rust/ql/test/query-tests/security/CWE-089/SqlInjection.expected new file mode 100644 index 000000000000..58f42bec0c84 --- /dev/null +++ b/rust/ql/test/query-tests/security/CWE-089/SqlInjection.expected @@ -0,0 +1,4 @@ +#select +edges +nodes +subpaths diff --git a/rust/ql/test/query-tests/security/CWE-089/SqlInjection.qlref b/rust/ql/test/query-tests/security/CWE-089/SqlInjection.qlref new file mode 100644 index 000000000000..504d27ff30cc --- /dev/null +++ b/rust/ql/test/query-tests/security/CWE-089/SqlInjection.qlref @@ -0,0 +1,2 @@ +query: queries/security/CWE-089/SqlInjection.ql +postprocess: utils/InlineExpectationsTestQuery.ql diff --git a/rust/ql/test/query-tests/security/CWE-089/cargo.toml.manual b/rust/ql/test/query-tests/security/CWE-089/cargo.toml.manual new file mode 100644 index 000000000000..0f9f0b75510e --- /dev/null +++ b/rust/ql/test/query-tests/security/CWE-089/cargo.toml.manual @@ -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" diff --git a/rust/ql/test/query-tests/security/CWE-089/migrations/20241031153051_create.sql b/rust/ql/test/query-tests/security/CWE-089/migrations/20241031153051_create.sql new file mode 100644 index 000000000000..c7d989a7258c --- /dev/null +++ b/rust/ql/test/query-tests/security/CWE-089/migrations/20241031153051_create.sql @@ -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"); diff --git a/rust/ql/test/query-tests/security/CWE-089/options.yml b/rust/ql/test/query-tests/security/CWE-089/options.yml new file mode 100644 index 000000000000..24744b7dfb45 --- /dev/null +++ b/rust/ql/test/query-tests/security/CWE-089/options.yml @@ -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" } diff --git a/rust/ql/test/query-tests/security/CWE-089/sqlx.rs b/rust/ql/test/query-tests/security/CWE-089/sqlx.rs new file mode 100644 index 000000000000..b5cc25000f99 --- /dev/null +++ b/rust/ql/test/query-tests/security/CWE-089/sqlx.rs @@ -0,0 +1,218 @@ +use sqlx::Connection; +use sqlx::Executor; + +/** + * This test is designed to be "run" in two ways: + * - you can extract and analyze the code here using the CodeQL test runner in the usual way, + * verifying the that various vulnerabilities are detected. + * - you can compile and run the code using `cargo`, verifying that it really is a complete + * program that compiles, runs and executes SQL commands (the sqlite ones, at least). + * + * To do the latter: + * + * Install `sqlx`: + * ``` + * cargo install sqlx-cli + * ``` + * + * Create the database: + * ``` + * export DATABASE_URL="sqlite:sqlite_database.db" + * sqlx db create + * sqlx migrate run + * ``` + * + * Build and run with the provided `cargo.toml.manual`: + * ``` + * cp cargo.toml.manual cargo.toml + * cargo run + * ``` + * + * You can also rebuild the sqlx 'query cache' in the `.sqlx` subdirectory + * with: + * ``` + * cargo sqlx prepare + * ``` + * This allows the code (in particular the `prepare!` macro) to be built + * in the test without setting `DATABASE_URL` first. + */ + +async fn test_sqlx_mysql(url: &str, enable_remote: bool) -> Result<(), sqlx::Error> { + // connect through a MySql connection pool + let pool = sqlx::mysql::MySqlPool::connect(url).await?; + let mut conn = pool.acquire().await?; + + // construct queries (with extra variants) + let const_string = String::from("Alice"); + let arg_string = std::env::args().nth(1).unwrap_or(String::from("Alice")); // $ MISSING Source=args1 + let remote_string = reqwest::blocking::get("http://example.com/").unwrap().text().unwrap_or(String::from("Alice")); // $ MISSING Source=remote1 + let remote_number = remote_string.parse::().unwrap_or(0); + let safe_query_1 = String::from("SELECT * FROM people WHERE firstname='Alice'"); + let safe_query_2 = String::from("SELECT * FROM people WHERE firstname='") + &const_string + "'"; + let safe_query_3 = format!("SELECT * FROM people WHERE firstname='{remote_number}'"); + let unsafe_query_1 = &arg_string; + let unsafe_query_2 = &remote_string; + let unsafe_query_3 = String::from("SELECT * FROM people WHERE firstname='") + &remote_string + "'"; + let unsafe_query_4 = format!("SELECT * FROM people WHERE firstname='{remote_string}'"); + let prepared_query_1 = String::from("SELECT * FROM people WHERE firstname=?"); // (prepared arguments are safe) + + // direct execution + let _ = conn.execute(safe_query_1.as_str()).await?; + let _ = conn.execute(safe_query_2.as_str()).await?; + let _ = conn.execute(safe_query_3.as_str()).await?; + let _ = conn.execute(unsafe_query_1.as_str()).await?; // $ MISSING Alert[sql-injection]=args1 + if enable_remote { + let _ = conn.execute(unsafe_query_2.as_str()).await?; // $ MISSING Alert[sql-injection]=remote1 + let _ = conn.execute(unsafe_query_3.as_str()).await?; // $ MISSING Alert[sql-injection]=remote1 + let _ = conn.execute(unsafe_query_4.as_str()).await?; // $ MISSING Alert[sql-injection]=remote1 + } + + // prepared queries + let _ = sqlx::query(safe_query_1.as_str()).execute(&pool).await?; + let _ = sqlx::query(safe_query_2.as_str()).execute(&pool).await?; + let _ = sqlx::query(safe_query_3.as_str()).execute(&pool).await?; + let _ = sqlx::query(unsafe_query_1.as_str()).execute(&pool).await?; // $ MISSING Alert[sql-injection]=args1 + if enable_remote { + let _ = sqlx::query(unsafe_query_2.as_str()).execute(&pool).await?; // $ MISSING Alert[sql-injection]=remote1 + let _ = sqlx::query(unsafe_query_3.as_str()).execute(&pool).await?; // $ MISSING Alert[sql-injection]=remote1 + let _ = sqlx::query(unsafe_query_4.as_str()).execute(&pool).await?; // $ MISSING Alert[sql-injection]=remote1 + } + let _ = sqlx::query(prepared_query_1.as_str()).bind(const_string).execute(&pool).await?; + let _ = sqlx::query(prepared_query_1.as_str()).bind(arg_string).execute(&pool).await?; + if enable_remote { + let _ = sqlx::query(prepared_query_1.as_str()).bind(remote_string).execute(&pool).await?; + let _ = sqlx::query(prepared_query_1.as_str()).bind(remote_number).execute(&pool).await?; + } + + Ok(()) +} + +async fn test_sqlx_sqlite(url: &str, enable_remote: bool) -> Result<(), sqlx::Error> { + // connect through Sqlite, no connection pool + let mut conn = sqlx::sqlite::SqliteConnection::connect(url).await?; + + // construct queries + let const_string = String::from("Alice"); + let remote_string = reqwest::blocking::get("http://example.com/").unwrap().text().unwrap_or(String::from("Alice")); // $ MISSING Source=remote2 + let safe_query_1 = String::from("SELECT * FROM people WHERE firstname='") + &const_string + "'"; + let unsafe_query_1 = String::from("SELECT * FROM people WHERE firstname='") + &remote_string + "'"; + let prepared_query_1 = String::from("SELECT * FROM people WHERE firstname=?"); // (prepared arguments are safe) + + // direct execution (with extra variants) + let _ = conn.execute(safe_query_1.as_str()).await?; + if enable_remote { + let _ = conn.execute(unsafe_query_1.as_str()).await?; // $ MISSING Alert[sql-injection]=remote2 + } + // ... + let _ = sqlx::raw_sql(safe_query_1.as_str()).execute(&mut conn).await?; + if enable_remote { + let _ = sqlx::raw_sql(unsafe_query_1.as_str()).execute(&mut conn).await?; // $ MISSING Alert[sql-injection]=remote2 + } + + // prepared queries (with extra variants) + let _ = sqlx::query(safe_query_1.as_str()).execute(&mut conn).await?; + let _ = sqlx::query(prepared_query_1.as_str()).bind(&const_string).execute(&mut conn).await?; + if enable_remote { + let _ = sqlx::query(unsafe_query_1.as_str()).execute(&mut conn).await?; // $ MISSING Alert[sql-injection]=remote2 + let _ = sqlx::query(prepared_query_1.as_str()).bind(&remote_string).execute(&mut conn).await?; + } + // ... + let _ = sqlx::query(safe_query_1.as_str()).fetch(&mut conn); + let _ = sqlx::query(prepared_query_1.as_str()).bind(&const_string).fetch(&mut conn); + if enable_remote { + let _ = sqlx::query(unsafe_query_1.as_str()).fetch(&mut conn); // $ MISSING Alert[sql-injection]=remote2 + let _ = sqlx::query(prepared_query_1.as_str()).bind(&remote_string).fetch(&mut conn); + } + // ... + let row1: (i64, String, String) = sqlx::query_as(safe_query_1.as_str()).fetch_one(&mut conn).await?; + println!(" row1 = {:?}", row1); + let row2: (i64, String, String) = sqlx::query_as(prepared_query_1.as_str()).bind(&const_string).fetch_one(&mut conn).await?; + println!(" row2 = {:?}", row2); + if enable_remote { + let _: (i64, String, String) = sqlx::query_as(unsafe_query_1.as_str()).fetch_one(&mut conn).await?; // $ MISSING Alert[sql-injection]=remote2 + let _: (i64, String, String) = sqlx::query_as(prepared_query_1.as_str()).bind(&remote_string).fetch_one(&mut conn).await?; + } + // ... + let row3: (i64, String, String) = sqlx::query_as(safe_query_1.as_str()).fetch_optional(&mut conn).await?.expect("no data"); + println!(" row3 = {:?}", row3); + let row4: (i64, String, String) = sqlx::query_as(prepared_query_1.as_str()).bind(&const_string).fetch_optional(&mut conn).await?.expect("no data"); + println!(" row4 = {:?}", row4); + if enable_remote { + let _: (i64, String, String) = sqlx::query_as(unsafe_query_1.as_str()).fetch_optional(&mut conn).await?.expect("no data"); // $ MISSING Alert[sql-injection]=remote2 + let _: (i64, String, String) = sqlx::query_as(prepared_query_1.as_str()).bind(&remote_string).fetch_optional(&mut conn).await?.expect("no data"); + } + // ... + let _ = sqlx::query(safe_query_1.as_str()).fetch_all(&mut conn).await?; + let _ = sqlx::query(prepared_query_1.as_str()).bind(&const_string).fetch_all(&mut conn).await?; + let _ = sqlx::query("SELECT * FROM people WHERE firstname=?").bind(&const_string).fetch_all(&mut conn).await?; + if enable_remote { + let _ = sqlx::query(unsafe_query_1.as_str()).fetch_all(&mut conn).await?; // $ MISSING Alert[sql-injection]=remote2 + let _ = sqlx::query(prepared_query_1.as_str()).bind(&remote_string).fetch_all(&mut conn).await?; + let _ = sqlx::query("SELECT * FROM people WHERE firstname=?").bind(&remote_string).fetch_all(&mut conn).await?; + } + // ... + let _ = sqlx::query!("SELECT * FROM people WHERE firstname=$1", const_string).fetch_all(&mut conn).await?; // (only takes string literals, so can't be vulnerable) + if enable_remote { + let _ = sqlx::query!("SELECT * FROM people WHERE firstname=$1", remote_string).fetch_all(&mut conn).await?; + } + + Ok(()) +} + +async fn test_sqlx_postgres(url: &str, enable_remote: bool) -> Result<(), sqlx::Error> { + // connect through a PostGres connection pool + let pool = sqlx::postgres::PgPool::connect(url).await?; + let mut conn = pool.acquire().await?; + + // construct queries + let const_string = String::from("Alice"); + let remote_string = reqwest::blocking::get("http://example.com/").unwrap().text().unwrap_or(String::from("Alice")); // $ MISSING Source=remote3 + let safe_query_1 = String::from("SELECT * FROM people WHERE firstname='") + &const_string + "'"; + let unsafe_query_1 = String::from("SELECT * FROM people WHERE firstname='") + &remote_string + "'"; + let prepared_query_1 = String::from("SELECT * FROM people WHERE firstname=$1"); // (prepared arguments are safe) + + // direct execution + let _ = conn.execute(safe_query_1.as_str()).await?; + if enable_remote { + let _ = conn.execute(unsafe_query_1.as_str()).await?; // $ MISSING Alert[sql-injection]=remote3 + } + + // prepared queries + let _ = sqlx::query(safe_query_1.as_str()).execute(&pool).await?; + let _ = sqlx::query(prepared_query_1.as_str()).bind(&const_string).execute(&pool).await?; + if enable_remote { + let _ = sqlx::query(unsafe_query_1.as_str()).execute(&pool).await?; // $ MISSING Alert[sql-injection]=remote3 + let _ = sqlx::query(prepared_query_1.as_str()).bind(&remote_string).execute(&pool).await?; + } + + Ok(()) +} + +fn main() { + println!("--- CWE-089 sqlx.rs test ---"); + + // we don't *actually* use data from a remote source unless we're explicitly told to at the + // command line; that's because this test is designed to be runnable, and we don't really + // want to expose the test database to potential SQL injection from http://example.com/ - + // no matter how unlikely, local and compartmentalized that may seem. + let enable_remote = std::env::args().nth(1) == Some(String::from("ENABLE_REMOTE")); + println!("enable_remote = {enable_remote}"); + + println!("test_sqlx_mysql..."); + match futures::executor::block_on(test_sqlx_mysql("", enable_remote)) { + Ok(_) => println!(" successful!"), + Err(e) => println!(" error: {}", e), + } + + println!("test_sqlx_sqlite..."); + match futures::executor::block_on(test_sqlx_sqlite("sqlite:sqlite_database.db", enable_remote)) { + Ok(_) => println!(" successful!"), + Err(e) => println!(" error: {}", e), + } + + println!("test_sqlx_postgres..."); + match futures::executor::block_on(test_sqlx_postgres("", enable_remote)) { + Ok(_) => println!(" successful!"), + Err(e) => println!(" error: {}", e), + } +}