diff --git a/rust/ql/lib/codeql/rust/Concepts.qll b/rust/ql/lib/codeql/rust/Concepts.qll index 5befe006bc69..9369668923f8 100644 --- a/rust/ql/lib/codeql/rust/Concepts.qll +++ b/rust/ql/lib/codeql/rust/Concepts.qll @@ -6,6 +6,7 @@ private import codeql.rust.dataflow.DataFlow private import codeql.threatmodels.ThreatModels +private import codeql.rust.Frameworks /** * A data flow source for a specific threat-model. @@ -46,6 +47,63 @@ class ActiveThreatModelSource extends ThreatModelSource { ActiveThreatModelSource() { currentThreatModel(this.getThreatModel()) } } +/** + * A data flow source corresponding to the program's command line arguments or path. + */ +final class CommandLineArgsSource = CommandLineArgsSource::Range; + +/** + * Provides a class for modeling new sources for the program's command line arguments or path. + */ +module CommandLineArgsSource { + /** + * A data flow source corresponding to the program's command line arguments or path. + */ + abstract class Range extends ThreatModelSource::Range { + override string getThreatModel() { result = "commandargs" } + + override string getSourceType() { result = "CommandLineArgs" } + } +} + +/** + * A data flow source corresponding to the program's environment. + */ +final class EnvironmentSource = EnvironmentSource::Range; + +/** + * Provides a class for modeling new sources for the program's environment. + */ +module EnvironmentSource { + /** + * A data flow source corresponding to the program's environment. + */ + abstract class Range extends ThreatModelSource::Range { + override string getThreatModel() { result = "environment" } + + override string getSourceType() { result = "EnvironmentSource" } + } +} + +/** + * A data flow source for remote (network) data. + */ +final class RemoteSource = RemoteSource::Range; + +/** + * Provides a class for modeling new sources of remote (network) data. + */ +module RemoteSource { + /** + * A data flow source for remote (network) data. + */ + abstract class Range extends ThreatModelSource::Range { + override string getThreatModel() { result = "remote" } + + override string getSourceType() { result = "RemoteSource" } + } +} + /** * A data-flow node that constructs a SQL statement. * diff --git a/rust/ql/lib/codeql/rust/Frameworks.qll b/rust/ql/lib/codeql/rust/Frameworks.qll new file mode 100644 index 000000000000..601e87ef6ebc --- /dev/null +++ b/rust/ql/lib/codeql/rust/Frameworks.qll @@ -0,0 +1,6 @@ +/** + * This file imports all models of frameworks and libraries. + */ + +private import codeql.rust.frameworks.Reqwest +private import codeql.rust.frameworks.stdlib.Env diff --git a/rust/ql/lib/codeql/rust/frameworks/Reqwest.qll b/rust/ql/lib/codeql/rust/frameworks/Reqwest.qll new file mode 100644 index 000000000000..2ab11a20ed4b --- /dev/null +++ b/rust/ql/lib/codeql/rust/frameworks/Reqwest.qll @@ -0,0 +1,19 @@ +/** + * Provides modeling for the `reqwest` library. + */ + +private import rust +private import codeql.rust.Concepts + +/** + * A call to `reqwest::get` or `reqwest::blocking::get`. + */ +private class ReqwestGet extends RemoteSource::Range { + ReqwestGet() { + exists(CallExpr ce | + this.asExpr().getExpr() = ce and + ce.getExpr().(PathExpr).getPath().getResolvedCrateOrigin().matches("%reqwest") and + ce.getExpr().(PathExpr).getPath().getResolvedPath() = ["crate::get", "crate::blocking::get"] + ) + } +} diff --git a/rust/ql/lib/codeql/rust/frameworks/stdlib/Env.qll b/rust/ql/lib/codeql/rust/frameworks/stdlib/Env.qll new file mode 100644 index 000000000000..69658d5fec53 --- /dev/null +++ b/rust/ql/lib/codeql/rust/frameworks/stdlib/Env.qll @@ -0,0 +1,36 @@ +/** + * Provides modeling for the `std::env` library. + */ + +private import rust +private import codeql.rust.Concepts + +/** + * A call to `std::env::args` or `std::env::args_os`. + */ +private class StdEnvArgs extends CommandLineArgsSource::Range { + StdEnvArgs() { + this.asExpr().getExpr().(CallExpr).getExpr().(PathExpr).getPath().getResolvedPath() = + ["crate::env::args", "crate::env::args_os"] + } +} + +/** + * A call to `std::env::current_dir`, `std::env::current_exe` or `std::env::home_dir`. + */ +private class StdEnvDir extends CommandLineArgsSource::Range { + StdEnvDir() { + this.asExpr().getExpr().(CallExpr).getExpr().(PathExpr).getPath().getResolvedPath() = + ["crate::env::current_dir", "crate::env::current_exe", "crate::env::home_dir"] + } +} + +/** + * A call to `std::env::var`, `std::env::var_os`, `std::env::vars` or `std::env::vars_os`. + */ +private class StdEnvVar extends EnvironmentSource::Range { + StdEnvVar() { + this.asExpr().getExpr().(CallExpr).getExpr().(PathExpr).getPath().getResolvedPath() = + ["crate::env::var", "crate::env::var_os", "crate::env::vars", "crate::env::vars_os"] + } +} diff --git a/rust/ql/src/queries/summary/SummaryStats.ql b/rust/ql/src/queries/summary/SummaryStats.ql index 72a876ba08ca..09ee83fc5e64 100644 --- a/rust/ql/src/queries/summary/SummaryStats.ql +++ b/rust/ql/src/queries/summary/SummaryStats.ql @@ -7,6 +7,7 @@ */ import rust +import codeql.rust.Concepts import codeql.rust.Diagnostics import Stats @@ -43,4 +44,8 @@ where key = "Macro calls - resolved" and value = count(MacroCall mc | mc.hasExpanded()) or key = "Macro calls - unresolved" and value = count(MacroCall mc | not mc.hasExpanded()) + or + key = "Taint sources - total" and value = count(ThreatModelSource s) + or + key = "Taint sources - active" and value = count(ActiveThreatModelSource s) select key, value order by key diff --git a/rust/ql/src/queries/summary/TaintSources.ql b/rust/ql/src/queries/summary/TaintSources.ql new file mode 100644 index 000000000000..9ac72b706efb --- /dev/null +++ b/rust/ql/src/queries/summary/TaintSources.ql @@ -0,0 +1,18 @@ +/** + * @name Taint Sources + * @description List all sources of untrusted input that have been idenfitied + * in the database. + * @kind problem + * @problem.severity info + * @id rust/summary/taint-sources + * @tags summary + */ + +import rust +import codeql.rust.Concepts + +from ThreatModelSource s, string defaultString +where + if s instanceof ActiveThreatModelSource then defaultString = " (DEFAULT)" else defaultString = "" +select s, + "Flow source '" + s.getSourceType() + "' of type " + s.getThreatModel() + defaultString + "." diff --git a/rust/ql/test/library-tests/dataflow/sources/InlineFlow.expected b/rust/ql/test/library-tests/dataflow/sources/InlineFlow.expected new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/rust/ql/test/library-tests/dataflow/sources/InlineFlow.ql b/rust/ql/test/library-tests/dataflow/sources/InlineFlow.ql new file mode 100644 index 000000000000..85ac4eec6983 --- /dev/null +++ b/rust/ql/test/library-tests/dataflow/sources/InlineFlow.ql @@ -0,0 +1,21 @@ +import rust +import codeql.rust.dataflow.DataFlow +import codeql.rust.Concepts +import utils.InlineFlowTest + +/** + * Configuration for flow from any threat model source to an argument of the function `sink`. + */ +module MyFlowConfig implements DataFlow::ConfigSig { + predicate isSource(DataFlow::Node source) { source instanceof ThreatModelSource } + + predicate isSink(DataFlow::Node sink) { + any(CallExpr call | call.getExpr().(PathExpr).getPath().getResolvedPath() = "crate::test::sink") + .getArgList() + .getAnArg() = sink.asExpr().getExpr() + } +} + +module MyFlowTest = TaintFlowTest; + +import MyFlowTest diff --git a/rust/ql/test/library-tests/dataflow/sources/TaintSources.expected b/rust/ql/test/library-tests/dataflow/sources/TaintSources.expected new file mode 100644 index 000000000000..cebbc00f3a5e --- /dev/null +++ b/rust/ql/test/library-tests/dataflow/sources/TaintSources.expected @@ -0,0 +1,17 @@ +| test.rs:8:10:8:30 | ...::var(...) | Flow source 'EnvironmentSource' of type environment. | +| test.rs:9:10:9:33 | ...::var_os(...) | Flow source 'EnvironmentSource' of type environment. | +| test.rs:11:16:11:36 | ...::var(...) | Flow source 'EnvironmentSource' of type environment. | +| test.rs:12:16:12:39 | ...::var_os(...) | Flow source 'EnvironmentSource' of type environment. | +| test.rs:17:25:17:40 | ...::vars(...) | Flow source 'EnvironmentSource' of type environment. | +| test.rs:22:25:22:43 | ...::vars_os(...) | Flow source 'EnvironmentSource' of type environment. | +| test.rs:29:29:29:44 | ...::args(...) | Flow source 'CommandLineArgs' of type commandargs. | +| test.rs:32:16:32:31 | ...::args(...) | Flow source 'CommandLineArgs' of type commandargs. | +| test.rs:33:16:33:34 | ...::args_os(...) | Flow source 'CommandLineArgs' of type commandargs. | +| test.rs:40:16:40:31 | ...::args(...) | Flow source 'CommandLineArgs' of type commandargs. | +| test.rs:44:16:44:34 | ...::args_os(...) | Flow source 'CommandLineArgs' of type commandargs. | +| test.rs:50:15:50:37 | ...::current_dir(...) | Flow source 'CommandLineArgs' of type commandargs. | +| test.rs:51:15:51:37 | ...::current_exe(...) | Flow source 'CommandLineArgs' of type commandargs. | +| test.rs:52:16:52:35 | ...::home_dir(...) | Flow source 'CommandLineArgs' of type commandargs. | +| test.rs:60:26:60:70 | ...::get(...) | Flow source 'RemoteSource' of type remote (DEFAULT). | +| test.rs:63:26:63:70 | ...::get(...) | Flow source 'RemoteSource' of type remote (DEFAULT). | +| test.rs:66:26:66:60 | ...::get(...) | Flow source 'RemoteSource' of type remote (DEFAULT). | diff --git a/rust/ql/test/library-tests/dataflow/sources/TaintSources.qlref b/rust/ql/test/library-tests/dataflow/sources/TaintSources.qlref new file mode 100644 index 000000000000..3f6de4d0e4e3 --- /dev/null +++ b/rust/ql/test/library-tests/dataflow/sources/TaintSources.qlref @@ -0,0 +1,2 @@ +query: queries/summary/TaintSources.ql +postprocess: utils/InlineExpectationsTestQuery.ql diff --git a/rust/ql/test/library-tests/dataflow/sources/options.yml b/rust/ql/test/library-tests/dataflow/sources/options.yml new file mode 100644 index 000000000000..3885768c7fa9 --- /dev/null +++ b/rust/ql/test/library-tests/dataflow/sources/options.yml @@ -0,0 +1,3 @@ +qltest_cargo_check: true +qltest_dependencies: + - reqwest = { version = "0.12.9", features = ["blocking"] } diff --git a/rust/ql/test/library-tests/dataflow/sources/reqwest.rs b/rust/ql/test/library-tests/dataflow/sources/reqwest.rs new file mode 100644 index 000000000000..3e8f5ef8510a --- /dev/null +++ b/rust/ql/test/library-tests/dataflow/sources/reqwest.rs @@ -0,0 +1,36 @@ + +// --- stubs for the "reqwest" library --- + +/* + --- we don't seem to have a way to use this, hence we currently test against the real reqwest library +#[derive(Debug)] +pub struct Error { } + +pub mod blocking { + pub struct Response { } + impl Response { + pub fn text(self) -> Result { + Ok("".to_string()) + } + } + + pub fn get(url: T) -> Result { + let _url = url; + + Ok(Response {}) + } +} + +pub struct Response { } +impl Response { + pub async fn text(self) -> Result { + Ok("".to_string()) + } +} + +pub async fn get(url: T) -> Result { + let _url = url; + + Ok(Response {}) +} +*/ diff --git a/rust/ql/test/library-tests/dataflow/sources/test.rs b/rust/ql/test/library-tests/dataflow/sources/test.rs new file mode 100644 index 000000000000..e4701865a7ea --- /dev/null +++ b/rust/ql/test/library-tests/dataflow/sources/test.rs @@ -0,0 +1,70 @@ +#![allow(deprecated)] + +fn sink(_: T) { } + +// --- tests --- + +fn test_env_vars() { + sink(std::env::var("HOME")); // $ Alert[rust/summary/taint-sources] hasTaintFlow + sink(std::env::var_os("PATH")); // $ Alert[rust/summary/taint-sources] hasTaintFlow + + let var1 = std::env::var("HOME").expect("HOME not set"); // $ Alert[rust/summary/taint-sources] + let var2 = std::env::var_os("PATH").unwrap(); // $ Alert[rust/summary/taint-sources] + + sink(var1); // $ MISSING: hasTaintFlow + sink(var2); // $ MISSING: hasTaintFlow + + for (key, value) in std::env::vars() { // $ Alert[rust/summary/taint-sources] + sink(key); // $ MISSING: hasTaintFlow + sink(value); // $ MISSING: hasTaintFlow + } + + for (key, value) in std::env::vars_os() { // $ Alert[rust/summary/taint-sources] + sink(key); // $ MISSING: hasTaintFlow + sink(value); // $ MISSING: hasTaintFlow + } +} + +fn test_env_args() { + let args: Vec = std::env::args().collect(); // $ Alert[rust/summary/taint-sources] + let my_path = &args[0]; + let arg1 = &args[1]; + let arg2 = std::env::args().nth(2).unwrap(); // $ Alert[rust/summary/taint-sources] + let arg3 = std::env::args_os().nth(3).unwrap(); // $ Alert[rust/summary/taint-sources] + + sink(my_path); // $ MISSING: hasTaintFlow + sink(arg1); // $ MISSING: hasTaintFlow + sink(arg2); // $ MISSING: hasTaintFlow + sink(arg3); // $ MISSING: hasTaintFlow + + for arg in std::env::args() { // $ Alert[rust/summary/taint-sources] + sink(arg); // $ MISSING: hasTaintFlow + } + + for arg in std::env::args_os() { // $ Alert[rust/summary/taint-sources] + sink(arg); // $ MISSING: hasTaintFlow + } +} + +fn test_env_dirs() { + let dir = std::env::current_dir().expect("FAILED"); // $ Alert[rust/summary/taint-sources] + let exe = std::env::current_exe().expect("FAILED"); // $ Alert[rust/summary/taint-sources] + let home = std::env::home_dir().expect("FAILED"); // $ Alert[rust/summary/taint-sources] + + sink(dir); // $ MISSING: hasTaintFlow + sink(exe); // $ MISSING: hasTaintFlow + sink(home); // $ MISSING: hasTaintFlow +} + +async fn test_reqwest() -> Result<(), reqwest::Error> { + let remote_string1 = reqwest::blocking::get("http://example.com/")?.text()?; // $ Alert[rust/summary/taint-sources] + sink(remote_string1); // $ MISSING: hasTaintFlow + + let remote_string2 = reqwest::blocking::get("http://example.com/").unwrap().text().unwrap(); // $ Alert[rust/summary/taint-sources] + sink(remote_string2); // $ MISSING: hasTaintFlow + + let remote_string3 = reqwest::get("http://example.com/").await?.text().await?; // $ Alert[rust/summary/taint-sources] + sink(remote_string3); // $ MISSING: hasTaintFlow + + Ok(()) +} diff --git a/rust/ql/test/query-tests/diagnostics/SummaryStats.expected b/rust/ql/test/query-tests/diagnostics/SummaryStats.expected index a3cf68f9b878..74e1e461c6fe 100644 --- a/rust/ql/test/query-tests/diagnostics/SummaryStats.expected +++ b/rust/ql/test/query-tests/diagnostics/SummaryStats.expected @@ -13,3 +13,5 @@ | Macro calls - resolved | 8 | | Macro calls - total | 9 | | Macro calls - unresolved | 1 | +| Taint sources - active | 0 | +| Taint sources - total | 0 |