From 494f91407096f461ded344845327760f589417cf Mon Sep 17 00:00:00 2001 From: Simon Friis Vindum Date: Fri, 7 Mar 2025 09:03:06 +0100 Subject: [PATCH 1/7] Rust: Add regular expression injection query --- .../hello-project/summary.expected | 2 +- .../hello-workspace/summary.cargo.expected | 2 +- .../summary.rust-project.expected | 2 +- .../codeql/rust/frameworks/regex.model.yml | 7 ++ .../regex/RegexInjectionExtensions.qll | 67 +++++++++++++++++++ .../security/regex/RegexInjectionQuery.qll | 31 +++++++++ .../security/CWE-730/RegexInjection.qhelp | 56 ++++++++++++++++ .../security/CWE-730/RegexInjection.ql | 20 ++++++ .../security/CWE-730/RegexInjectionBad.rs | 8 +++ .../security/CWE-730/RegexInjectionGood.rs | 9 +++ .../dataflow/local/DataFlowStep.expected | 1 + .../diagnostics/SummaryStats.expected | 2 +- .../security/CWE-730/RegexInjection.expected | 28 ++++++++ .../security/CWE-730/RegexInjection.qlref | 2 + .../CWE-730/RegexInjectionSink.expected | 2 + .../security/CWE-730/RegexInjectionSink.ql | 4 ++ .../test/query-tests/security/CWE-730/main.rs | 36 ++++++++++ .../query-tests/security/CWE-730/options.yml | 3 + 18 files changed, 278 insertions(+), 4 deletions(-) create mode 100644 rust/ql/lib/codeql/rust/frameworks/regex.model.yml create mode 100644 rust/ql/lib/codeql/rust/security/regex/RegexInjectionExtensions.qll create mode 100644 rust/ql/lib/codeql/rust/security/regex/RegexInjectionQuery.qll create mode 100644 rust/ql/src/queries/security/CWE-730/RegexInjection.qhelp create mode 100644 rust/ql/src/queries/security/CWE-730/RegexInjection.ql create mode 100644 rust/ql/src/queries/security/CWE-730/RegexInjectionBad.rs create mode 100644 rust/ql/src/queries/security/CWE-730/RegexInjectionGood.rs create mode 100644 rust/ql/test/query-tests/security/CWE-730/RegexInjection.expected create mode 100644 rust/ql/test/query-tests/security/CWE-730/RegexInjection.qlref create mode 100644 rust/ql/test/query-tests/security/CWE-730/RegexInjectionSink.expected create mode 100644 rust/ql/test/query-tests/security/CWE-730/RegexInjectionSink.ql create mode 100644 rust/ql/test/query-tests/security/CWE-730/main.rs create mode 100644 rust/ql/test/query-tests/security/CWE-730/options.yml diff --git a/rust/ql/integration-tests/hello-project/summary.expected b/rust/ql/integration-tests/hello-project/summary.expected index 2ffb1f4e34f7..e5a531c16bab 100644 --- a/rust/ql/integration-tests/hello-project/summary.expected +++ b/rust/ql/integration-tests/hello-project/summary.expected @@ -14,7 +14,7 @@ | Macro calls - resolved | 2 | | Macro calls - total | 2 | | Macro calls - unresolved | 0 | -| Taint edges - number of edges | 1670 | +| Taint edges - number of edges | 1671 | | Taint reach - nodes tainted | 0 | | Taint reach - per million nodes | 0 | | Taint sinks - cryptographic operations | 0 | diff --git a/rust/ql/integration-tests/hello-workspace/summary.cargo.expected b/rust/ql/integration-tests/hello-workspace/summary.cargo.expected index d08ce1a41166..084025a1bb9f 100644 --- a/rust/ql/integration-tests/hello-workspace/summary.cargo.expected +++ b/rust/ql/integration-tests/hello-workspace/summary.cargo.expected @@ -14,7 +14,7 @@ | Macro calls - resolved | 2 | | Macro calls - total | 2 | | Macro calls - unresolved | 0 | -| Taint edges - number of edges | 1670 | +| Taint edges - number of edges | 1671 | | Taint reach - nodes tainted | 0 | | Taint reach - per million nodes | 0 | | Taint sinks - cryptographic operations | 0 | diff --git a/rust/ql/integration-tests/hello-workspace/summary.rust-project.expected b/rust/ql/integration-tests/hello-workspace/summary.rust-project.expected index d08ce1a41166..084025a1bb9f 100644 --- a/rust/ql/integration-tests/hello-workspace/summary.rust-project.expected +++ b/rust/ql/integration-tests/hello-workspace/summary.rust-project.expected @@ -14,7 +14,7 @@ | Macro calls - resolved | 2 | | Macro calls - total | 2 | | Macro calls - unresolved | 0 | -| Taint edges - number of edges | 1670 | +| Taint edges - number of edges | 1671 | | Taint reach - nodes tainted | 0 | | Taint reach - per million nodes | 0 | | Taint sinks - cryptographic operations | 0 | diff --git a/rust/ql/lib/codeql/rust/frameworks/regex.model.yml b/rust/ql/lib/codeql/rust/frameworks/regex.model.yml new file mode 100644 index 000000000000..b645dc0b955c --- /dev/null +++ b/rust/ql/lib/codeql/rust/frameworks/regex.model.yml @@ -0,0 +1,7 @@ +# Models for the `regex` crate. +extensions: + - addsTo: + pack: codeql/rust-all + extensible: summaryModel + data: + - ["repo:https://github.com/rust-lang/regex:regex", "crate::escape", "Argument[0].Reference", "ReturnValue", "taint", "manual"] diff --git a/rust/ql/lib/codeql/rust/security/regex/RegexInjectionExtensions.qll b/rust/ql/lib/codeql/rust/security/regex/RegexInjectionExtensions.qll new file mode 100644 index 000000000000..7e886146c2cf --- /dev/null +++ b/rust/ql/lib/codeql/rust/security/regex/RegexInjectionExtensions.qll @@ -0,0 +1,67 @@ +/** + * Provides classes and predicates to reason about regular expression injection + * vulnerabilities. + */ + +private import codeql.util.Unit +private import rust +private import codeql.rust.dataflow.DataFlow +private import codeql.rust.controlflow.CfgNodes +private import codeql.rust.dataflow.FlowSink + +/** + * A data flow sink for regular expression injection vulnerabilities. + */ +abstract class RegexInjectionSink extends DataFlow::Node { } + +/** + * A barrier for regular expression injection vulnerabilities. + */ +abstract class RegexInjectionBarrier extends DataFlow::Node { } + +/** A sink for `a` in `Regex::new(a)` when `a` is not a literal. */ +private class NewRegexInjectionSink extends RegexInjectionSink { + NewRegexInjectionSink() { + exists(CallExprCfgNode call, PathExpr path | + path = call.getFunction().getExpr() and + path.getResolvedCrateOrigin() = "repo:https://github.com/rust-lang/regex:regex" and + path.getResolvedPath() = "::new" and + this.asExpr() = call.getArgument(0) and + not this.asExpr() instanceof LiteralExprCfgNode + ) + } +} + +private class MadRegexInjectionSink extends RegexInjectionSink { + MadRegexInjectionSink() { sinkNode(this, "regex-use") } +} + +/** + * A unit class for adding additional flow steps. + */ +class RegexInjectionAdditionalFlowStep extends Unit { + /** + * Holds if the step from `node1` to `node2` should be considered a flow + * step for paths related to regular expression injection vulnerabilities. + */ + abstract predicate step(DataFlow::Node node1, DataFlow::Node node2); +} + +/** + * An escape barrier for regular expression injection vulnerabilities. + */ +private class RegexInjectionDefaultBarrier extends RegexInjectionBarrier { + RegexInjectionDefaultBarrier() { + // A barrier is any call to a function named `escape`, in particular this + // makes calls to `regex::escape` a barrier. + this.asExpr() + .getExpr() + .(CallExpr) + .getFunction() + .(PathExpr) + .getPath() + .getPart() + .getNameRef() + .getText() = "escape" + } +} diff --git a/rust/ql/lib/codeql/rust/security/regex/RegexInjectionQuery.qll b/rust/ql/lib/codeql/rust/security/regex/RegexInjectionQuery.qll new file mode 100644 index 000000000000..387b8caae309 --- /dev/null +++ b/rust/ql/lib/codeql/rust/security/regex/RegexInjectionQuery.qll @@ -0,0 +1,31 @@ +/** + * Provides a taint-tracking configuration for detecting regular expression + * injection vulnerabilities. + */ + +private import rust +private import codeql.rust.dataflow.DataFlow +private import codeql.rust.dataflow.TaintTracking +private import codeql.rust.dataflow.FlowSource +private import codeql.rust.Concepts +private import codeql.rust.security.regex.RegexInjectionExtensions + +/** + * A taint configuration for detecting regular expression injection vulnerabilities. + */ +module RegexInjectionConfig implements DataFlow::ConfigSig { + predicate isSource(DataFlow::Node source) { source instanceof ThreatModelSource } + + predicate isSink(DataFlow::Node sink) { sink instanceof RegexInjectionSink } + + predicate isBarrier(DataFlow::Node barrier) { barrier instanceof RegexInjectionBarrier } + + predicate isAdditionalFlowStep(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) { + any(RegexInjectionAdditionalFlowStep s).step(nodeFrom, nodeTo) + } +} + +/** + * Detect taint flow of tainted data that reaches a regular expression sink. + */ +module RegexInjectionFlow = TaintTracking::Global; diff --git a/rust/ql/src/queries/security/CWE-730/RegexInjection.qhelp b/rust/ql/src/queries/security/CWE-730/RegexInjection.qhelp new file mode 100644 index 000000000000..046b172d3e7a --- /dev/null +++ b/rust/ql/src/queries/security/CWE-730/RegexInjection.qhelp @@ -0,0 +1,56 @@ + + + + +

+Constructing a regular expression with unsanitized user input can be dangerous. +A malicious user may be able to modify the meaning of the expression causing it +to match unexpected strings and to construct large regular expressions by using +counted repetitions. +

+
+ + +

+Before embedding user input into a regular expression, escape the input string +using a function such as regex::escape to +escape meta-characters that have special meaning. +

+

+If purposefully supporting user supplied regular expressions, then use RegexBuilder::size_limit +to limit the pattern size such that it is no larger than necessary. +

+
+ + +

+The following example construct a regular expressions from the user input +key without escaping it first. +

+ + + +

+The regular expression is intended to match strings starting with +"property" such as "property:foo=bar". However, a +malicious user might inject the regular expression ".*^|key" and +unexpectedly cause strings such as "key=secret" to match. +

+

+If user input is used to construct a regular expression it should be escaped +first. This ensures that the user cannot insert characters that have special +meanings in regular expressions. +

+ +
+ + +
  • + regex crate documentation: Untrusted patterns +
  • +
    +
    diff --git a/rust/ql/src/queries/security/CWE-730/RegexInjection.ql b/rust/ql/src/queries/security/CWE-730/RegexInjection.ql new file mode 100644 index 000000000000..88a85eb48583 --- /dev/null +++ b/rust/ql/src/queries/security/CWE-730/RegexInjection.ql @@ -0,0 +1,20 @@ +/** + * @name Regular expression injection + * @description + * @kind path-problem + * @problem.severity error + * @security-severity 7.5 + * @precision high + * @id rust/regex-injection + * @tags security + * external/cwe/cwe-730 + * external/cwe/cwe-400 + */ + +private import codeql.rust.security.regex.RegexInjectionQuery +private import RegexInjectionFlow::PathGraph + +from RegexInjectionFlow::PathNode sourceNode, RegexInjectionFlow::PathNode sinkNode +where RegexInjectionFlow::flowPath(sourceNode, sinkNode) +select sinkNode.getNode(), sourceNode, sinkNode, + "This regular expression is constructed from a $@.", sourceNode.getNode(), "user-provided value" diff --git a/rust/ql/src/queries/security/CWE-730/RegexInjectionBad.rs b/rust/ql/src/queries/security/CWE-730/RegexInjectionBad.rs new file mode 100644 index 000000000000..5d5bd78becc6 --- /dev/null +++ b/rust/ql/src/queries/security/CWE-730/RegexInjectionBad.rs @@ -0,0 +1,8 @@ +use regex::Regex; + +fn get_value<'h>(key: &str, property: &'h str) -> Option<&'h str> { + // BAD: User provided `key` is interpolated into the regular expression. + let pattern = format!(r"^property:{key}=(.*)$"); + let re = Regex::new(&pattern).unwrap(); + re.captures(property)?.get(1).map(|m| m.as_str()) +} \ No newline at end of file diff --git a/rust/ql/src/queries/security/CWE-730/RegexInjectionGood.rs b/rust/ql/src/queries/security/CWE-730/RegexInjectionGood.rs new file mode 100644 index 000000000000..fa87435acdc0 --- /dev/null +++ b/rust/ql/src/queries/security/CWE-730/RegexInjectionGood.rs @@ -0,0 +1,9 @@ +use regex::{escape, Regex}; + +fn get_value<'h>(key: &str, property: &'h str) -> option<&'h str> { + // GOOD: User input is escaped before being used in the regular expression. + let escaped_key = escape(key); + let pattern = format!(r"^property:{escaped_key}=(.*)$"); + let re = regex::new(&pattern).unwrap(); + re.captures(property)?.get(1).map(|m| m.as_str()) +} \ No newline at end of file diff --git a/rust/ql/test/library-tests/dataflow/local/DataFlowStep.expected b/rust/ql/test/library-tests/dataflow/local/DataFlowStep.expected index e403311345cc..8a8c5988426a 100644 --- a/rust/ql/test/library-tests/dataflow/local/DataFlowStep.expected +++ b/rust/ql/test/library-tests/dataflow/local/DataFlowStep.expected @@ -2493,6 +2493,7 @@ readStep | file://:0:0:0:0 | [summary param] 0 in lang:std::_::crate::sys_common::ignore_notfound | Err | file://:0:0:0:0 | [summary] read: Argument[0].Field[crate::result::Result::Err(0)] in lang:std::_::crate::sys_common::ignore_notfound | | file://:0:0:0:0 | [summary param] 0 in lang:std::_::crate::thread::current::try_with_current | function return | file://:0:0:0:0 | [summary] read: Argument[0].ReturnValue in lang:std::_::crate::thread::current::try_with_current | | file://:0:0:0:0 | [summary param] 0 in lang:std::_::crate::thread::with_current_name | function return | file://:0:0:0:0 | [summary] read: Argument[0].ReturnValue in lang:std::_::crate::thread::with_current_name | +| file://:0:0:0:0 | [summary param] 0 in repo:https://github.com/rust-lang/regex:regex::_::crate::escape | &ref | file://:0:0:0:0 | [summary] read: Argument[0].Reference in repo:https://github.com/rust-lang/regex:regex::_::crate::escape | | file://:0:0:0:0 | [summary param] 1 in lang:alloc::_::::fold | function return | file://:0:0:0:0 | [summary] read: Argument[1].ReturnValue in lang:alloc::_::::fold | | file://:0:0:0:0 | [summary param] 1 in lang:alloc::_::crate::collections::btree::mem::replace | function return | file://:0:0:0:0 | [summary] read: Argument[1].ReturnValue in lang:alloc::_::crate::collections::btree::mem::replace | | file://:0:0:0:0 | [summary param] 1 in lang:alloc::_::crate::collections::btree::mem::take_mut | function return | file://:0:0:0:0 | [summary] read: Argument[1].ReturnValue in lang:alloc::_::crate::collections::btree::mem::take_mut | diff --git a/rust/ql/test/query-tests/diagnostics/SummaryStats.expected b/rust/ql/test/query-tests/diagnostics/SummaryStats.expected index 5249eae29b1f..7c7c2dd1961b 100644 --- a/rust/ql/test/query-tests/diagnostics/SummaryStats.expected +++ b/rust/ql/test/query-tests/diagnostics/SummaryStats.expected @@ -14,7 +14,7 @@ | Macro calls - resolved | 8 | | Macro calls - total | 9 | | Macro calls - unresolved | 1 | -| Taint edges - number of edges | 1670 | +| Taint edges - number of edges | 1671 | | Taint reach - nodes tainted | 0 | | Taint reach - per million nodes | 0 | | Taint sinks - cryptographic operations | 0 | diff --git a/rust/ql/test/query-tests/security/CWE-730/RegexInjection.expected b/rust/ql/test/query-tests/security/CWE-730/RegexInjection.expected new file mode 100644 index 000000000000..fd7ded696334 --- /dev/null +++ b/rust/ql/test/query-tests/security/CWE-730/RegexInjection.expected @@ -0,0 +1,28 @@ +#select +| main.rs:6:25:6:30 | ®ex | main.rs:4:20:4:32 | ...::var | main.rs:6:25:6:30 | ®ex | This regular expression is constructed from a $@. | main.rs:4:20:4:32 | ...::var | user-provided value | +edges +| main.rs:4:9:4:16 | username | main.rs:5:25:5:44 | MacroExpr | provenance | | +| main.rs:4:20:4:32 | ...::var | main.rs:4:20:4:40 | ...::var(...) [Ok] | provenance | Src:MaD:60 | +| main.rs:4:20:4:40 | ...::var(...) [Ok] | main.rs:4:20:4:66 | ... .unwrap_or(...) | provenance | MaD:1573 | +| main.rs:4:20:4:66 | ... .unwrap_or(...) | main.rs:4:9:4:16 | username | provenance | | +| main.rs:5:9:5:13 | regex | main.rs:6:26:6:30 | regex | provenance | | +| main.rs:5:17:5:45 | res | main.rs:5:25:5:44 | { ... } | provenance | | +| main.rs:5:25:5:44 | ...::format(...) | main.rs:5:17:5:45 | res | provenance | | +| main.rs:5:25:5:44 | ...::must_use(...) | main.rs:5:9:5:13 | regex | provenance | | +| main.rs:5:25:5:44 | MacroExpr | main.rs:5:25:5:44 | ...::format(...) | provenance | MaD:64 | +| main.rs:5:25:5:44 | { ... } | main.rs:5:25:5:44 | ...::must_use(...) | provenance | MaD:2996 | +| main.rs:6:26:6:30 | regex | main.rs:6:25:6:30 | ®ex | provenance | | +nodes +| main.rs:4:9:4:16 | username | semmle.label | username | +| main.rs:4:20:4:32 | ...::var | semmle.label | ...::var | +| main.rs:4:20:4:40 | ...::var(...) [Ok] | semmle.label | ...::var(...) [Ok] | +| main.rs:4:20:4:66 | ... .unwrap_or(...) | semmle.label | ... .unwrap_or(...) | +| main.rs:5:9:5:13 | regex | semmle.label | regex | +| main.rs:5:17:5:45 | res | semmle.label | res | +| main.rs:5:25:5:44 | ...::format(...) | semmle.label | ...::format(...) | +| main.rs:5:25:5:44 | ...::must_use(...) | semmle.label | ...::must_use(...) | +| main.rs:5:25:5:44 | MacroExpr | semmle.label | MacroExpr | +| main.rs:5:25:5:44 | { ... } | semmle.label | { ... } | +| main.rs:6:25:6:30 | ®ex | semmle.label | ®ex | +| main.rs:6:26:6:30 | regex | semmle.label | regex | +subpaths diff --git a/rust/ql/test/query-tests/security/CWE-730/RegexInjection.qlref b/rust/ql/test/query-tests/security/CWE-730/RegexInjection.qlref new file mode 100644 index 000000000000..d62d882f992d --- /dev/null +++ b/rust/ql/test/query-tests/security/CWE-730/RegexInjection.qlref @@ -0,0 +1,2 @@ +query: queries/security/CWE-730/RegexInjection.ql +postprocess: utils/test/InlineExpectationsTestQuery.ql diff --git a/rust/ql/test/query-tests/security/CWE-730/RegexInjectionSink.expected b/rust/ql/test/query-tests/security/CWE-730/RegexInjectionSink.expected new file mode 100644 index 000000000000..4ea46a385870 --- /dev/null +++ b/rust/ql/test/query-tests/security/CWE-730/RegexInjectionSink.expected @@ -0,0 +1,2 @@ +| main.rs:6:25:6:30 | ®ex | +| main.rs:14:25:14:30 | ®ex | diff --git a/rust/ql/test/query-tests/security/CWE-730/RegexInjectionSink.ql b/rust/ql/test/query-tests/security/CWE-730/RegexInjectionSink.ql new file mode 100644 index 000000000000..0723ec5bb5b7 --- /dev/null +++ b/rust/ql/test/query-tests/security/CWE-730/RegexInjectionSink.ql @@ -0,0 +1,4 @@ +private import codeql.rust.dataflow.DataFlow +private import codeql.rust.security.regex.RegexInjectionExtensions + +query predicate regexInjectionSink(DataFlow::Node node) { node instanceof RegexInjectionSink } diff --git a/rust/ql/test/query-tests/security/CWE-730/main.rs b/rust/ql/test/query-tests/security/CWE-730/main.rs new file mode 100644 index 000000000000..a03e5d4b4942 --- /dev/null +++ b/rust/ql/test/query-tests/security/CWE-730/main.rs @@ -0,0 +1,36 @@ +use regex::Regex; + +fn simple_bad(hay: &str) -> Option { + let username = std::env::var("USER").unwrap_or("".to_string()); // $ Source=env + let regex = format!("foo{}bar", username); + let re = Regex::new(®ex).unwrap(); // $ Alert[rust/regex-injection]=env + Some(re.is_match(hay)) +} + +fn simple_good(hay: &str) -> Option { + let username = std::env::var("USER").unwrap_or("".to_string()); + let escaped = regex::escape(&username); + let regex = format!("foo{}bar", escaped); + let re = Regex::new(®ex).unwrap(); + Some(re.is_match(hay)) +} + +fn not_a_sink_literal() -> Option { + let username = std::env::var("USER").unwrap_or("".to_string()); + let re = Regex::new("literal string").unwrap(); + Some(re.is_match(&username)) +} + +fn not_a_sink_raw_literal() -> Option { + let username = std::env::var("USER").unwrap_or("".to_string()); + let re = Regex::new(r"literal string").unwrap(); + Some(re.is_match(&username)) +} + +fn main() { + let hay = "a string"; + simple_bad(hay); + simple_good(hay); + not_a_sink_literal(); + not_a_sink_raw_literal(); +} diff --git a/rust/ql/test/query-tests/security/CWE-730/options.yml b/rust/ql/test/query-tests/security/CWE-730/options.yml new file mode 100644 index 000000000000..8d484623101f --- /dev/null +++ b/rust/ql/test/query-tests/security/CWE-730/options.yml @@ -0,0 +1,3 @@ +qltest_cargo_check: true +qltest_dependencies: + - regex = { version = "1.11.1" } From 179ea041f45967a1c570f305d88802d8d36abe64 Mon Sep 17 00:00:00 2001 From: Simon Friis Vindum Date: Mon, 10 Mar 2025 09:09:13 +0100 Subject: [PATCH 2/7] Rust: Merge query implementation into one file --- .../security/regex/RegexInjectionQuery.qll | 31 ------------------- .../security/CWE-730/RegexInjection.ql | 27 +++++++++++++++- 2 files changed, 26 insertions(+), 32 deletions(-) delete mode 100644 rust/ql/lib/codeql/rust/security/regex/RegexInjectionQuery.qll diff --git a/rust/ql/lib/codeql/rust/security/regex/RegexInjectionQuery.qll b/rust/ql/lib/codeql/rust/security/regex/RegexInjectionQuery.qll deleted file mode 100644 index 387b8caae309..000000000000 --- a/rust/ql/lib/codeql/rust/security/regex/RegexInjectionQuery.qll +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Provides a taint-tracking configuration for detecting regular expression - * injection vulnerabilities. - */ - -private import rust -private import codeql.rust.dataflow.DataFlow -private import codeql.rust.dataflow.TaintTracking -private import codeql.rust.dataflow.FlowSource -private import codeql.rust.Concepts -private import codeql.rust.security.regex.RegexInjectionExtensions - -/** - * A taint configuration for detecting regular expression injection vulnerabilities. - */ -module RegexInjectionConfig implements DataFlow::ConfigSig { - predicate isSource(DataFlow::Node source) { source instanceof ThreatModelSource } - - predicate isSink(DataFlow::Node sink) { sink instanceof RegexInjectionSink } - - predicate isBarrier(DataFlow::Node barrier) { barrier instanceof RegexInjectionBarrier } - - predicate isAdditionalFlowStep(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) { - any(RegexInjectionAdditionalFlowStep s).step(nodeFrom, nodeTo) - } -} - -/** - * Detect taint flow of tainted data that reaches a regular expression sink. - */ -module RegexInjectionFlow = TaintTracking::Global; diff --git a/rust/ql/src/queries/security/CWE-730/RegexInjection.ql b/rust/ql/src/queries/security/CWE-730/RegexInjection.ql index 88a85eb48583..64cfff45cb09 100644 --- a/rust/ql/src/queries/security/CWE-730/RegexInjection.ql +++ b/rust/ql/src/queries/security/CWE-730/RegexInjection.ql @@ -11,7 +11,32 @@ * external/cwe/cwe-400 */ -private import codeql.rust.security.regex.RegexInjectionQuery +private import rust +private import codeql.rust.dataflow.DataFlow +private import codeql.rust.dataflow.TaintTracking +private import codeql.rust.Concepts +private import codeql.rust.security.regex.RegexInjectionExtensions + +/** + * A taint configuration for detecting regular expression injection vulnerabilities. + */ +module RegexInjectionConfig implements DataFlow::ConfigSig { + predicate isSource(DataFlow::Node source) { source instanceof ThreatModelSource } + + predicate isSink(DataFlow::Node sink) { sink instanceof RegexInjectionSink } + + predicate isBarrier(DataFlow::Node barrier) { barrier instanceof RegexInjectionBarrier } + + predicate isAdditionalFlowStep(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) { + any(RegexInjectionAdditionalFlowStep s).step(nodeFrom, nodeTo) + } +} + +/** + * Detect taint flow of tainted data that reaches a regular expression sink. + */ +module RegexInjectionFlow = TaintTracking::Global; + private import RegexInjectionFlow::PathGraph from RegexInjectionFlow::PathNode sourceNode, RegexInjectionFlow::PathNode sinkNode From 344fea21283f09c7d51323d6cd9efb90647b1318 Mon Sep 17 00:00:00 2001 From: Simon Friis Vindum Date: Mon, 10 Mar 2025 13:23:20 +0100 Subject: [PATCH 3/7] Rust: Enable local threat models in tests and use active threat models for regex query --- rust/ql/src/queries/security/CWE-730/RegexInjection.ql | 2 +- rust/ql/test/default-threat-models.model.yml | 6 ++++++ rust/ql/test/qlpack.yml | 2 ++ 3 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 rust/ql/test/default-threat-models.model.yml diff --git a/rust/ql/src/queries/security/CWE-730/RegexInjection.ql b/rust/ql/src/queries/security/CWE-730/RegexInjection.ql index 64cfff45cb09..0d50d8ead6d8 100644 --- a/rust/ql/src/queries/security/CWE-730/RegexInjection.ql +++ b/rust/ql/src/queries/security/CWE-730/RegexInjection.ql @@ -21,7 +21,7 @@ private import codeql.rust.security.regex.RegexInjectionExtensions * A taint configuration for detecting regular expression injection vulnerabilities. */ module RegexInjectionConfig implements DataFlow::ConfigSig { - predicate isSource(DataFlow::Node source) { source instanceof ThreatModelSource } + predicate isSource(DataFlow::Node source) { source instanceof ActiveThreatModelSource } predicate isSink(DataFlow::Node sink) { sink instanceof RegexInjectionSink } diff --git a/rust/ql/test/default-threat-models.model.yml b/rust/ql/test/default-threat-models.model.yml new file mode 100644 index 000000000000..63507f477386 --- /dev/null +++ b/rust/ql/test/default-threat-models.model.yml @@ -0,0 +1,6 @@ +extensions: + - addsTo: + pack: codeql/threat-models + extensible: threatModelConfiguration + data: + - ["local", true, 0] diff --git a/rust/ql/test/qlpack.yml b/rust/ql/test/qlpack.yml index 7428ca33bed7..1bf759f71e09 100644 --- a/rust/ql/test/qlpack.yml +++ b/rust/ql/test/qlpack.yml @@ -6,3 +6,5 @@ dependencies: extractor: rust tests: . warnOnImplicitThis: true +dataExtensions: + - default-threat-models.model.yml From 0e965f7616a5bbd1a8df76c00a7648b58925c672 Mon Sep 17 00:00:00 2001 From: Simon Friis Vindum Date: Mon, 10 Mar 2025 14:39:37 +0100 Subject: [PATCH 4/7] Rust: Accept changes --- .../dataflow/sources/TaintSources.expected | 32 ++++---- .../security/CWE-089/SqlInjection.expected | 76 +++++++++++++------ .../test/query-tests/security/CWE-089/sqlx.rs | 6 +- 3 files changed, 71 insertions(+), 43 deletions(-) diff --git a/rust/ql/test/library-tests/dataflow/sources/TaintSources.expected b/rust/ql/test/library-tests/dataflow/sources/TaintSources.expected index 603ba0d76058..8448ee783720 100644 --- a/rust/ql/test/library-tests/dataflow/sources/TaintSources.expected +++ b/rust/ql/test/library-tests/dataflow/sources/TaintSources.expected @@ -1,18 +1,18 @@ -| test.rs:8:10:8:22 | ...::var | Flow source 'EnvironmentSource' of type environment. | -| test.rs:9:10:9:25 | ...::var_os | Flow source 'EnvironmentSource' of type environment. | -| test.rs:11:16:11:28 | ...::var | Flow source 'EnvironmentSource' of type environment. | -| test.rs:12:16:12:31 | ...::var_os | Flow source 'EnvironmentSource' of type environment. | -| test.rs:17:25:17:38 | ...::vars | Flow source 'EnvironmentSource' of type environment. | -| test.rs:22:25:22:41 | ...::vars_os | Flow source 'EnvironmentSource' of type environment. | -| test.rs:29:29:29:42 | ...::args | Flow source 'CommandLineArgs' of type commandargs. | -| test.rs:32:16:32:29 | ...::args | Flow source 'CommandLineArgs' of type commandargs. | -| test.rs:33:16:33:32 | ...::args_os | Flow source 'CommandLineArgs' of type commandargs. | -| test.rs:34:16:34:29 | ...::args | Flow source 'CommandLineArgs' of type commandargs. | -| test.rs:42:16:42:29 | ...::args | Flow source 'CommandLineArgs' of type commandargs. | -| test.rs:46:16:46:32 | ...::args_os | Flow source 'CommandLineArgs' of type commandargs. | -| test.rs:52:15:52:35 | ...::current_dir | Flow source 'CommandLineArgs' of type commandargs. | -| test.rs:53:15:53:35 | ...::current_exe | Flow source 'CommandLineArgs' of type commandargs. | -| test.rs:54:16:54:33 | ...::home_dir | Flow source 'CommandLineArgs' of type commandargs. | +| test.rs:8:10:8:22 | ...::var | Flow source 'EnvironmentSource' of type environment (DEFAULT). | +| test.rs:9:10:9:25 | ...::var_os | Flow source 'EnvironmentSource' of type environment (DEFAULT). | +| test.rs:11:16:11:28 | ...::var | Flow source 'EnvironmentSource' of type environment (DEFAULT). | +| test.rs:12:16:12:31 | ...::var_os | Flow source 'EnvironmentSource' of type environment (DEFAULT). | +| test.rs:17:25:17:38 | ...::vars | Flow source 'EnvironmentSource' of type environment (DEFAULT). | +| test.rs:22:25:22:41 | ...::vars_os | Flow source 'EnvironmentSource' of type environment (DEFAULT). | +| test.rs:29:29:29:42 | ...::args | Flow source 'CommandLineArgs' of type commandargs (DEFAULT). | +| test.rs:32:16:32:29 | ...::args | Flow source 'CommandLineArgs' of type commandargs (DEFAULT). | +| test.rs:33:16:33:32 | ...::args_os | Flow source 'CommandLineArgs' of type commandargs (DEFAULT). | +| test.rs:34:16:34:29 | ...::args | Flow source 'CommandLineArgs' of type commandargs (DEFAULT). | +| test.rs:42:16:42:29 | ...::args | Flow source 'CommandLineArgs' of type commandargs (DEFAULT). | +| test.rs:46:16:46:32 | ...::args_os | Flow source 'CommandLineArgs' of type commandargs (DEFAULT). | +| test.rs:52:15:52:35 | ...::current_dir | Flow source 'CommandLineArgs' of type commandargs (DEFAULT). | +| test.rs:53:15:53:35 | ...::current_exe | Flow source 'CommandLineArgs' of type commandargs (DEFAULT). | +| test.rs:54:16:54:33 | ...::home_dir | Flow source 'CommandLineArgs' of type commandargs (DEFAULT). | | test.rs:62:26:62:47 | ...::get | Flow source 'RemoteSource' of type remote (DEFAULT). | | test.rs:65:26:65:47 | ...::get | Flow source 'RemoteSource' of type remote (DEFAULT). | | test.rs:68:26:68:47 | ...::get | Flow source 'RemoteSource' of type remote (DEFAULT). | @@ -22,4 +22,4 @@ | test.rs:80:24:80:35 | ...::get | Flow source 'RemoteSource' of type remote (DEFAULT). | | test.rs:112:35:112:46 | send_request | Flow source 'RemoteSource' of type remote (DEFAULT). | | test.rs:119:31:119:42 | send_request | Flow source 'RemoteSource' of type remote (DEFAULT). | -| test.rs:203:16:203:29 | ...::args | Flow source 'CommandLineArgs' of type commandargs. | +| test.rs:203:16:203:29 | ...::args | Flow source 'CommandLineArgs' of type commandargs (DEFAULT). | diff --git a/rust/ql/test/query-tests/security/CWE-089/SqlInjection.expected b/rust/ql/test/query-tests/security/CWE-089/SqlInjection.expected index 6b04453ec9ae..0d6f031a18f0 100644 --- a/rust/ql/test/query-tests/security/CWE-089/SqlInjection.expected +++ b/rust/ql/test/query-tests/security/CWE-089/SqlInjection.expected @@ -1,52 +1,73 @@ #select | sqlx.rs:62:26:62:46 | safe_query_3.as_str(...) | sqlx.rs:48:25:48:46 | ...::get | sqlx.rs:62:26:62:46 | safe_query_3.as_str(...) | This query depends on a $@. | sqlx.rs:48:25:48:46 | ...::get | user-provided value | +| sqlx.rs:63:26:63:48 | unsafe_query_1.as_str(...) | sqlx.rs:47:22:47:35 | ...::args | sqlx.rs:63:26:63:48 | unsafe_query_1.as_str(...) | This query depends on a $@. | sqlx.rs:47:22:47:35 | ...::args | user-provided value | | sqlx.rs:65:30:65:52 | unsafe_query_2.as_str(...) | sqlx.rs:48:25:48:46 | ...::get | sqlx.rs:65:30:65:52 | unsafe_query_2.as_str(...) | This query depends on a $@. | sqlx.rs:48:25:48:46 | ...::get | user-provided value | | sqlx.rs:67:30:67:52 | unsafe_query_4.as_str(...) | sqlx.rs:48:25:48:46 | ...::get | sqlx.rs:67:30:67:52 | unsafe_query_4.as_str(...) | This query depends on a $@. | sqlx.rs:48:25:48:46 | ...::get | user-provided value | | sqlx.rs:73:25:73:45 | safe_query_3.as_str(...) | sqlx.rs:48:25:48:46 | ...::get | sqlx.rs:73:25:73:45 | safe_query_3.as_str(...) | This query depends on a $@. | sqlx.rs:48:25:48:46 | ...::get | user-provided value | +| sqlx.rs:74:25:74:47 | unsafe_query_1.as_str(...) | sqlx.rs:47:22:47:35 | ...::args | sqlx.rs:74:25:74:47 | unsafe_query_1.as_str(...) | This query depends on a $@. | sqlx.rs:47:22:47:35 | ...::args | user-provided value | | sqlx.rs:76:29:76:51 | unsafe_query_2.as_str(...) | sqlx.rs:48:25:48:46 | ...::get | sqlx.rs:76:29:76:51 | unsafe_query_2.as_str(...) | This query depends on a $@. | sqlx.rs:48:25:48:46 | ...::get | user-provided value | | sqlx.rs:78:29:78:51 | unsafe_query_4.as_str(...) | sqlx.rs:48:25:48:46 | ...::get | sqlx.rs:78:29:78:51 | unsafe_query_4.as_str(...) | This query depends on a $@. | sqlx.rs:48:25:48:46 | ...::get | user-provided value | edges -| sqlx.rs:48:9:48:21 | remote_string | sqlx.rs:49:25:49:52 | remote_string.parse(...) [Ok] | provenance | MaD:6 | +| sqlx.rs:47:9:47:18 | arg_string | sqlx.rs:53:27:53:36 | arg_string | provenance | | +| sqlx.rs:47:22:47:35 | ...::args | sqlx.rs:47:22:47:37 | ...::args(...) [element] | provenance | Src:MaD:1 | +| sqlx.rs:47:22:47:37 | ...::args(...) [element] | sqlx.rs:47:22:47:44 | ... .nth(...) [Some] | provenance | MaD:10 | +| sqlx.rs:47:22:47:44 | ... .nth(...) [Some] | sqlx.rs:47:22:47:77 | ... .unwrap_or(...) | provenance | MaD:5 | +| sqlx.rs:47:22:47:77 | ... .unwrap_or(...) | sqlx.rs:47:9:47:18 | arg_string | provenance | | +| sqlx.rs:48:9:48:21 | remote_string | sqlx.rs:49:25:49:52 | remote_string.parse(...) [Ok] | provenance | MaD:8 | | sqlx.rs:48:9:48:21 | remote_string | sqlx.rs:54:27:54:39 | remote_string | provenance | | | sqlx.rs:48:9:48:21 | remote_string | sqlx.rs:56:34:56:89 | MacroExpr | provenance | | -| sqlx.rs:48:25:48:46 | ...::get | sqlx.rs:48:25:48:69 | ...::get(...) [Ok] | provenance | Src:MaD:1 | -| sqlx.rs:48:25:48:69 | ...::get(...) [Ok] | sqlx.rs:48:25:48:78 | ... .unwrap(...) | provenance | MaD:4 | -| sqlx.rs:48:25:48:78 | ... .unwrap(...) | sqlx.rs:48:25:48:85 | ... .text(...) [Ok] | provenance | MaD:8 | -| sqlx.rs:48:25:48:85 | ... .text(...) [Ok] | sqlx.rs:48:25:48:118 | ... .unwrap_or(...) | provenance | MaD:5 | +| sqlx.rs:48:25:48:46 | ...::get | sqlx.rs:48:25:48:69 | ...::get(...) [Ok] | provenance | Src:MaD:2 | +| sqlx.rs:48:25:48:69 | ...::get(...) [Ok] | sqlx.rs:48:25:48:78 | ... .unwrap(...) | provenance | MaD:6 | +| sqlx.rs:48:25:48:78 | ... .unwrap(...) | sqlx.rs:48:25:48:85 | ... .text(...) [Ok] | provenance | MaD:11 | +| sqlx.rs:48:25:48:85 | ... .text(...) [Ok] | sqlx.rs:48:25:48:118 | ... .unwrap_or(...) | provenance | MaD:7 | | sqlx.rs:48:25:48:118 | ... .unwrap_or(...) | sqlx.rs:48:9:48:21 | remote_string | provenance | | | sqlx.rs:49:9:49:21 | remote_number | sqlx.rs:52:32:52:87 | MacroExpr | provenance | | -| sqlx.rs:49:25:49:52 | remote_string.parse(...) [Ok] | sqlx.rs:49:25:49:65 | ... .unwrap_or(...) | provenance | MaD:5 | +| sqlx.rs:49:25:49:52 | remote_string.parse(...) [Ok] | sqlx.rs:49:25:49:65 | ... .unwrap_or(...) | provenance | MaD:7 | | sqlx.rs:49:25:49:65 | ... .unwrap_or(...) | sqlx.rs:49:9:49:21 | remote_number | provenance | | -| sqlx.rs:52:9:52:20 | safe_query_3 | sqlx.rs:62:26:62:46 | safe_query_3.as_str(...) | provenance | MaD:2 | -| sqlx.rs:52:9:52:20 | safe_query_3 | sqlx.rs:73:25:73:45 | safe_query_3.as_str(...) | provenance | MaD:2 | +| sqlx.rs:52:9:52:20 | safe_query_3 | sqlx.rs:62:26:62:46 | safe_query_3.as_str(...) | provenance | MaD:3 | +| sqlx.rs:52:9:52:20 | safe_query_3 | sqlx.rs:73:25:73:45 | safe_query_3.as_str(...) | provenance | MaD:3 | | sqlx.rs:52:24:52:88 | res | sqlx.rs:52:32:52:87 | { ... } | provenance | | | sqlx.rs:52:32:52:87 | ...::format(...) | sqlx.rs:52:24:52:88 | res | provenance | | | sqlx.rs:52:32:52:87 | ...::must_use(...) | sqlx.rs:52:9:52:20 | safe_query_3 | provenance | | -| sqlx.rs:52:32:52:87 | MacroExpr | sqlx.rs:52:32:52:87 | ...::format(...) | provenance | MaD:3 | -| sqlx.rs:52:32:52:87 | { ... } | sqlx.rs:52:32:52:87 | ...::must_use(...) | provenance | MaD:7 | +| sqlx.rs:52:32:52:87 | MacroExpr | sqlx.rs:52:32:52:87 | ...::format(...) | provenance | MaD:4 | +| sqlx.rs:52:32:52:87 | { ... } | sqlx.rs:52:32:52:87 | ...::must_use(...) | provenance | MaD:9 | +| sqlx.rs:53:9:53:22 | unsafe_query_1 [&ref] | sqlx.rs:63:26:63:39 | unsafe_query_1 [&ref] | provenance | | +| sqlx.rs:53:9:53:22 | unsafe_query_1 [&ref] | sqlx.rs:74:25:74:38 | unsafe_query_1 [&ref] | provenance | | +| sqlx.rs:53:26:53:36 | &arg_string [&ref] | sqlx.rs:53:9:53:22 | unsafe_query_1 [&ref] | provenance | | +| sqlx.rs:53:27:53:36 | arg_string | sqlx.rs:53:26:53:36 | &arg_string [&ref] | provenance | | | sqlx.rs:54:9:54:22 | unsafe_query_2 [&ref] | sqlx.rs:65:30:65:43 | unsafe_query_2 [&ref] | provenance | | | sqlx.rs:54:9:54:22 | unsafe_query_2 [&ref] | sqlx.rs:76:29:76:42 | unsafe_query_2 [&ref] | provenance | | | sqlx.rs:54:26:54:39 | &remote_string [&ref] | sqlx.rs:54:9:54:22 | unsafe_query_2 [&ref] | provenance | | | sqlx.rs:54:27:54:39 | remote_string | sqlx.rs:54:26:54:39 | &remote_string [&ref] | provenance | | -| sqlx.rs:56:9:56:22 | unsafe_query_4 | sqlx.rs:67:30:67:52 | unsafe_query_4.as_str(...) | provenance | MaD:2 | -| sqlx.rs:56:9:56:22 | unsafe_query_4 | sqlx.rs:78:29:78:51 | unsafe_query_4.as_str(...) | provenance | MaD:2 | +| sqlx.rs:56:9:56:22 | unsafe_query_4 | sqlx.rs:67:30:67:52 | unsafe_query_4.as_str(...) | provenance | MaD:3 | +| sqlx.rs:56:9:56:22 | unsafe_query_4 | sqlx.rs:78:29:78:51 | unsafe_query_4.as_str(...) | provenance | MaD:3 | | sqlx.rs:56:26:56:90 | res | sqlx.rs:56:34:56:89 | { ... } | provenance | | | sqlx.rs:56:34:56:89 | ...::format(...) | sqlx.rs:56:26:56:90 | res | provenance | | | sqlx.rs:56:34:56:89 | ...::must_use(...) | sqlx.rs:56:9:56:22 | unsafe_query_4 | provenance | | -| sqlx.rs:56:34:56:89 | MacroExpr | sqlx.rs:56:34:56:89 | ...::format(...) | provenance | MaD:3 | -| sqlx.rs:56:34:56:89 | { ... } | sqlx.rs:56:34:56:89 | ...::must_use(...) | provenance | MaD:7 | -| sqlx.rs:65:30:65:43 | unsafe_query_2 [&ref] | sqlx.rs:65:30:65:52 | unsafe_query_2.as_str(...) | provenance | MaD:2 | -| sqlx.rs:76:29:76:42 | unsafe_query_2 [&ref] | sqlx.rs:76:29:76:51 | unsafe_query_2.as_str(...) | provenance | MaD:2 | +| sqlx.rs:56:34:56:89 | MacroExpr | sqlx.rs:56:34:56:89 | ...::format(...) | provenance | MaD:4 | +| sqlx.rs:56:34:56:89 | { ... } | sqlx.rs:56:34:56:89 | ...::must_use(...) | provenance | MaD:9 | +| sqlx.rs:63:26:63:39 | unsafe_query_1 [&ref] | sqlx.rs:63:26:63:48 | unsafe_query_1.as_str(...) | provenance | MaD:3 | +| sqlx.rs:65:30:65:43 | unsafe_query_2 [&ref] | sqlx.rs:65:30:65:52 | unsafe_query_2.as_str(...) | provenance | MaD:3 | +| sqlx.rs:74:25:74:38 | unsafe_query_1 [&ref] | sqlx.rs:74:25:74:47 | unsafe_query_1.as_str(...) | provenance | MaD:3 | +| sqlx.rs:76:29:76:42 | unsafe_query_2 [&ref] | sqlx.rs:76:29:76:51 | unsafe_query_2.as_str(...) | provenance | MaD:3 | models -| 1 | Source: repo:https://github.com/seanmonstar/reqwest:reqwest; crate::blocking::get; remote; ReturnValue.Field[crate::result::Result::Ok(0)] | -| 2 | Summary: lang:alloc; ::as_str; Argument[self]; ReturnValue; taint | -| 3 | Summary: lang:alloc; crate::fmt::format; Argument[0]; ReturnValue; taint | -| 4 | Summary: lang:core; ::unwrap; Argument[self].Field[crate::result::Result::Ok(0)]; ReturnValue; value | -| 5 | Summary: lang:core; ::unwrap_or; Argument[self].Field[crate::result::Result::Ok(0)]; ReturnValue; value | -| 6 | Summary: lang:core; ::parse; Argument[self]; ReturnValue.Field[crate::result::Result::Ok(0)]; taint | -| 7 | Summary: lang:core; crate::hint::must_use; Argument[0]; ReturnValue; value | -| 8 | Summary: repo:https://github.com/seanmonstar/reqwest:reqwest; ::text; Argument[self]; ReturnValue.Field[crate::result::Result::Ok(0)]; taint | +| 1 | Source: lang:std; crate::env::args; command-line-source; ReturnValue.Element | +| 2 | Source: repo:https://github.com/seanmonstar/reqwest:reqwest; crate::blocking::get; remote; ReturnValue.Field[crate::result::Result::Ok(0)] | +| 3 | Summary: lang:alloc; ::as_str; Argument[self]; ReturnValue; taint | +| 4 | Summary: lang:alloc; crate::fmt::format; Argument[0]; ReturnValue; taint | +| 5 | Summary: lang:core; ::unwrap_or; Argument[self].Field[crate::option::Option::Some(0)]; ReturnValue; value | +| 6 | Summary: lang:core; ::unwrap; Argument[self].Field[crate::result::Result::Ok(0)]; ReturnValue; value | +| 7 | Summary: lang:core; ::unwrap_or; Argument[self].Field[crate::result::Result::Ok(0)]; ReturnValue; value | +| 8 | Summary: lang:core; ::parse; Argument[self]; ReturnValue.Field[crate::result::Result::Ok(0)]; taint | +| 9 | Summary: lang:core; crate::hint::must_use; Argument[0]; ReturnValue; value | +| 10 | Summary: lang:core; crate::iter::traits::iterator::Iterator::nth; Argument[self].Element; ReturnValue.Field[crate::option::Option::Some(0)]; value | +| 11 | Summary: repo:https://github.com/seanmonstar/reqwest:reqwest; ::text; Argument[self]; ReturnValue.Field[crate::result::Result::Ok(0)]; taint | nodes +| sqlx.rs:47:9:47:18 | arg_string | semmle.label | arg_string | +| sqlx.rs:47:22:47:35 | ...::args | semmle.label | ...::args | +| sqlx.rs:47:22:47:37 | ...::args(...) [element] | semmle.label | ...::args(...) [element] | +| sqlx.rs:47:22:47:44 | ... .nth(...) [Some] | semmle.label | ... .nth(...) [Some] | +| sqlx.rs:47:22:47:77 | ... .unwrap_or(...) | semmle.label | ... .unwrap_or(...) | | sqlx.rs:48:9:48:21 | remote_string | semmle.label | remote_string | | sqlx.rs:48:25:48:46 | ...::get | semmle.label | ...::get | | sqlx.rs:48:25:48:69 | ...::get(...) [Ok] | semmle.label | ...::get(...) [Ok] | @@ -62,6 +83,9 @@ nodes | sqlx.rs:52:32:52:87 | ...::must_use(...) | semmle.label | ...::must_use(...) | | sqlx.rs:52:32:52:87 | MacroExpr | semmle.label | MacroExpr | | sqlx.rs:52:32:52:87 | { ... } | semmle.label | { ... } | +| sqlx.rs:53:9:53:22 | unsafe_query_1 [&ref] | semmle.label | unsafe_query_1 [&ref] | +| sqlx.rs:53:26:53:36 | &arg_string [&ref] | semmle.label | &arg_string [&ref] | +| sqlx.rs:53:27:53:36 | arg_string | semmle.label | arg_string | | sqlx.rs:54:9:54:22 | unsafe_query_2 [&ref] | semmle.label | unsafe_query_2 [&ref] | | sqlx.rs:54:26:54:39 | &remote_string [&ref] | semmle.label | &remote_string [&ref] | | sqlx.rs:54:27:54:39 | remote_string | semmle.label | remote_string | @@ -72,10 +96,14 @@ nodes | sqlx.rs:56:34:56:89 | MacroExpr | semmle.label | MacroExpr | | sqlx.rs:56:34:56:89 | { ... } | semmle.label | { ... } | | sqlx.rs:62:26:62:46 | safe_query_3.as_str(...) | semmle.label | safe_query_3.as_str(...) | +| sqlx.rs:63:26:63:39 | unsafe_query_1 [&ref] | semmle.label | unsafe_query_1 [&ref] | +| sqlx.rs:63:26:63:48 | unsafe_query_1.as_str(...) | semmle.label | unsafe_query_1.as_str(...) | | sqlx.rs:65:30:65:43 | unsafe_query_2 [&ref] | semmle.label | unsafe_query_2 [&ref] | | sqlx.rs:65:30:65:52 | unsafe_query_2.as_str(...) | semmle.label | unsafe_query_2.as_str(...) | | sqlx.rs:67:30:67:52 | unsafe_query_4.as_str(...) | semmle.label | unsafe_query_4.as_str(...) | | sqlx.rs:73:25:73:45 | safe_query_3.as_str(...) | semmle.label | safe_query_3.as_str(...) | +| sqlx.rs:74:25:74:38 | unsafe_query_1 [&ref] | semmle.label | unsafe_query_1 [&ref] | +| sqlx.rs:74:25:74:47 | unsafe_query_1.as_str(...) | semmle.label | unsafe_query_1.as_str(...) | | sqlx.rs:76:29:76:42 | unsafe_query_2 [&ref] | semmle.label | unsafe_query_2 [&ref] | | sqlx.rs:76:29:76:51 | unsafe_query_2.as_str(...) | semmle.label | unsafe_query_2.as_str(...) | | sqlx.rs:78:29:78:51 | unsafe_query_4.as_str(...) | semmle.label | unsafe_query_4.as_str(...) | diff --git a/rust/ql/test/query-tests/security/CWE-089/sqlx.rs b/rust/ql/test/query-tests/security/CWE-089/sqlx.rs index d40a4bb2a118..f051cf49e08d 100644 --- a/rust/ql/test/query-tests/security/CWE-089/sqlx.rs +++ b/rust/ql/test/query-tests/security/CWE-089/sqlx.rs @@ -44,7 +44,7 @@ async fn test_sqlx_mysql(url: &str, enable_remote: bool) -> Result<(), sqlx::Err // 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 arg_string = std::env::args().nth(1).unwrap_or(String::from("Alice")); // $ Source=args1 let remote_string = reqwest::blocking::get("http://example.com/").unwrap().text().unwrap_or(String::from("Alice")); // $ Source=remote1 let remote_number = remote_string.parse::().unwrap_or(0); let safe_query_1 = String::from("SELECT * FROM people WHERE firstname='Alice'"); @@ -60,7 +60,7 @@ async fn test_sqlx_mysql(url: &str, enable_remote: bool) -> Result<(), sqlx::Err let _ = conn.execute(safe_query_1.as_str()).await?; // $ sql-sink let _ = conn.execute(safe_query_2.as_str()).await?; // $ sql-sink let _ = conn.execute(safe_query_3.as_str()).await?; // $ sql-sink SPURIOUS: Alert[rust/sql-injection]=remote1 - let _ = conn.execute(unsafe_query_1.as_str()).await?; // $ sql-sink MISSING: Alert[rust/sql-injection]=args1 + let _ = conn.execute(unsafe_query_1.as_str()).await?; // $ sql-sink Alert[rust/sql-injection]=args1 if enable_remote { let _ = conn.execute(unsafe_query_2.as_str()).await?; // $ sql-sink Alert[rust/sql-injection]=remote1 let _ = conn.execute(unsafe_query_3.as_str()).await?; // $ sql-sink MISSING: Alert[rust/sql-injection]=remote1 @@ -71,7 +71,7 @@ async fn test_sqlx_mysql(url: &str, enable_remote: bool) -> Result<(), sqlx::Err let _ = sqlx::query(safe_query_1.as_str()).execute(&pool).await?; // $ sql-sink let _ = sqlx::query(safe_query_2.as_str()).execute(&pool).await?; // $ sql-sink let _ = sqlx::query(safe_query_3.as_str()).execute(&pool).await?; // $ sql-sink SPURIOUS: Alert[rust/sql-injection]=remote1 - let _ = sqlx::query(unsafe_query_1.as_str()).execute(&pool).await?; // $ sql-sink MISSING: Alert[rust/sql-injection][rust/sql-injection]=args1 + let _ = sqlx::query(unsafe_query_1.as_str()).execute(&pool).await?; // $ sql-sink Alert[rust/sql-injection]=args1 if enable_remote { let _ = sqlx::query(unsafe_query_2.as_str()).execute(&pool).await?; // $ sql-sink Alert[rust/sql-injection]=remote1 let _ = sqlx::query(unsafe_query_3.as_str()).execute(&pool).await?; // $ sql-sink MISSING: Alert[rust/sql-injection]=remote1 From 5c83644360fe95a580dd7ef49293bfdafd1caaa7 Mon Sep 17 00:00:00 2001 From: Simon Friis Vindum Date: Mon, 10 Mar 2025 14:52:25 +0100 Subject: [PATCH 5/7] Rust: Use CWE 20 for regex injection query --- .../security/{CWE-730 => CWE-020}/RegexInjection.qhelp | 0 .../queries/security/{CWE-730 => CWE-020}/RegexInjection.ql | 6 +++--- .../security/{CWE-730 => CWE-020}/RegexInjectionBad.rs | 0 .../security/{CWE-730 => CWE-020}/RegexInjectionGood.rs | 0 .../security/{CWE-730 => CWE-020}/RegexInjection.expected | 0 .../security/{CWE-730 => CWE-020}/RegexInjection.qlref | 2 +- .../{CWE-730 => CWE-020}/RegexInjectionSink.expected | 0 .../security/{CWE-730 => CWE-020}/RegexInjectionSink.ql | 0 .../test/query-tests/security/{CWE-730 => CWE-020}/main.rs | 0 .../query-tests/security/{CWE-730 => CWE-020}/options.yml | 0 10 files changed, 4 insertions(+), 4 deletions(-) rename rust/ql/src/queries/security/{CWE-730 => CWE-020}/RegexInjection.qhelp (100%) rename rust/ql/src/queries/security/{CWE-730 => CWE-020}/RegexInjection.ql (94%) rename rust/ql/src/queries/security/{CWE-730 => CWE-020}/RegexInjectionBad.rs (100%) rename rust/ql/src/queries/security/{CWE-730 => CWE-020}/RegexInjectionGood.rs (100%) rename rust/ql/test/query-tests/security/{CWE-730 => CWE-020}/RegexInjection.expected (100%) rename rust/ql/test/query-tests/security/{CWE-730 => CWE-020}/RegexInjection.qlref (52%) rename rust/ql/test/query-tests/security/{CWE-730 => CWE-020}/RegexInjectionSink.expected (100%) rename rust/ql/test/query-tests/security/{CWE-730 => CWE-020}/RegexInjectionSink.ql (100%) rename rust/ql/test/query-tests/security/{CWE-730 => CWE-020}/main.rs (100%) rename rust/ql/test/query-tests/security/{CWE-730 => CWE-020}/options.yml (100%) diff --git a/rust/ql/src/queries/security/CWE-730/RegexInjection.qhelp b/rust/ql/src/queries/security/CWE-020/RegexInjection.qhelp similarity index 100% rename from rust/ql/src/queries/security/CWE-730/RegexInjection.qhelp rename to rust/ql/src/queries/security/CWE-020/RegexInjection.qhelp diff --git a/rust/ql/src/queries/security/CWE-730/RegexInjection.ql b/rust/ql/src/queries/security/CWE-020/RegexInjection.ql similarity index 94% rename from rust/ql/src/queries/security/CWE-730/RegexInjection.ql rename to rust/ql/src/queries/security/CWE-020/RegexInjection.ql index 0d50d8ead6d8..0f3bd1ed85a2 100644 --- a/rust/ql/src/queries/security/CWE-730/RegexInjection.ql +++ b/rust/ql/src/queries/security/CWE-020/RegexInjection.ql @@ -3,12 +3,12 @@ * @description * @kind path-problem * @problem.severity error - * @security-severity 7.5 + * @security-severity 7.8 * @precision high * @id rust/regex-injection * @tags security - * external/cwe/cwe-730 - * external/cwe/cwe-400 + * external/cwe/cwe-020 + * external/cwe/cwe-074 */ private import rust diff --git a/rust/ql/src/queries/security/CWE-730/RegexInjectionBad.rs b/rust/ql/src/queries/security/CWE-020/RegexInjectionBad.rs similarity index 100% rename from rust/ql/src/queries/security/CWE-730/RegexInjectionBad.rs rename to rust/ql/src/queries/security/CWE-020/RegexInjectionBad.rs diff --git a/rust/ql/src/queries/security/CWE-730/RegexInjectionGood.rs b/rust/ql/src/queries/security/CWE-020/RegexInjectionGood.rs similarity index 100% rename from rust/ql/src/queries/security/CWE-730/RegexInjectionGood.rs rename to rust/ql/src/queries/security/CWE-020/RegexInjectionGood.rs diff --git a/rust/ql/test/query-tests/security/CWE-730/RegexInjection.expected b/rust/ql/test/query-tests/security/CWE-020/RegexInjection.expected similarity index 100% rename from rust/ql/test/query-tests/security/CWE-730/RegexInjection.expected rename to rust/ql/test/query-tests/security/CWE-020/RegexInjection.expected diff --git a/rust/ql/test/query-tests/security/CWE-730/RegexInjection.qlref b/rust/ql/test/query-tests/security/CWE-020/RegexInjection.qlref similarity index 52% rename from rust/ql/test/query-tests/security/CWE-730/RegexInjection.qlref rename to rust/ql/test/query-tests/security/CWE-020/RegexInjection.qlref index d62d882f992d..bc028b7e20d6 100644 --- a/rust/ql/test/query-tests/security/CWE-730/RegexInjection.qlref +++ b/rust/ql/test/query-tests/security/CWE-020/RegexInjection.qlref @@ -1,2 +1,2 @@ -query: queries/security/CWE-730/RegexInjection.ql +query: queries/security/CWE-020/RegexInjection.ql postprocess: utils/test/InlineExpectationsTestQuery.ql diff --git a/rust/ql/test/query-tests/security/CWE-730/RegexInjectionSink.expected b/rust/ql/test/query-tests/security/CWE-020/RegexInjectionSink.expected similarity index 100% rename from rust/ql/test/query-tests/security/CWE-730/RegexInjectionSink.expected rename to rust/ql/test/query-tests/security/CWE-020/RegexInjectionSink.expected diff --git a/rust/ql/test/query-tests/security/CWE-730/RegexInjectionSink.ql b/rust/ql/test/query-tests/security/CWE-020/RegexInjectionSink.ql similarity index 100% rename from rust/ql/test/query-tests/security/CWE-730/RegexInjectionSink.ql rename to rust/ql/test/query-tests/security/CWE-020/RegexInjectionSink.ql diff --git a/rust/ql/test/query-tests/security/CWE-730/main.rs b/rust/ql/test/query-tests/security/CWE-020/main.rs similarity index 100% rename from rust/ql/test/query-tests/security/CWE-730/main.rs rename to rust/ql/test/query-tests/security/CWE-020/main.rs diff --git a/rust/ql/test/query-tests/security/CWE-730/options.yml b/rust/ql/test/query-tests/security/CWE-020/options.yml similarity index 100% rename from rust/ql/test/query-tests/security/CWE-730/options.yml rename to rust/ql/test/query-tests/security/CWE-020/options.yml From b48fd99913261250b0c137d9f5594e602f7f1fb4 Mon Sep 17 00:00:00 2001 From: Simon Friis Vindum Date: Mon, 10 Mar 2025 16:30:52 +0100 Subject: [PATCH 6/7] Rust: Applying suggestions to documentation --- .../queries/security/CWE-020/RegexInjection.qhelp | 14 +++++++------- .../src/queries/security/CWE-020/RegexInjection.ql | 5 ++++- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/rust/ql/src/queries/security/CWE-020/RegexInjection.qhelp b/rust/ql/src/queries/security/CWE-020/RegexInjection.qhelp index 046b172d3e7a..f314de2042e2 100644 --- a/rust/ql/src/queries/security/CWE-020/RegexInjection.qhelp +++ b/rust/ql/src/queries/security/CWE-020/RegexInjection.qhelp @@ -6,8 +6,8 @@

    Constructing a regular expression with unsanitized user input can be dangerous. -A malicious user may be able to modify the meaning of the expression causing it -to match unexpected strings and to construct large regular expressions by using +A malicious user may be able to modify the meaning of the expression, causing it +to match unexpected strings and construct large regular expressions by using counted repetitions.

    @@ -22,13 +22,13 @@ escape meta-characters that have special meaning.

    If purposefully supporting user supplied regular expressions, then use RegexBuilder::size_limit -to limit the pattern size such that it is no larger than necessary. +to limit the pattern size so that it is no larger than necessary.

    -The following example construct a regular expressions from the user input +The following example constructs a regular expressions from the user input key without escaping it first.

    @@ -41,8 +41,8 @@ malicious user might inject the regular expression ".*^|key" and unexpectedly cause strings such as "key=secret" to match.

    -If user input is used to construct a regular expression it should be escaped -first. This ensures that the user cannot insert characters that have special +If user input is used to construct a regular expression, it should be escaped +first. This ensures that the malicious users cannot insert characters that have special meanings in regular expressions.

    @@ -50,7 +50,7 @@ meanings in regular expressions.
  • - regex crate documentation: Untrusted patterns + regex crate documentation: Untrusted patterns.
  • diff --git a/rust/ql/src/queries/security/CWE-020/RegexInjection.ql b/rust/ql/src/queries/security/CWE-020/RegexInjection.ql index 0f3bd1ed85a2..b49d08969f62 100644 --- a/rust/ql/src/queries/security/CWE-020/RegexInjection.ql +++ b/rust/ql/src/queries/security/CWE-020/RegexInjection.ql @@ -1,6 +1,9 @@ /** * @name Regular expression injection - * @description + * @description User input should not be used in regular expressions without first being + * escaped, otherwise a malicious user may be able to inject an expression that + * could modify the meaning of the expression, causing it to match unexpected + * strings. * @kind path-problem * @problem.severity error * @security-severity 7.8 From 1e0b78ebd37a206243734bb57a20697b99af2958 Mon Sep 17 00:00:00 2001 From: Simon Friis Vindum Date: Tue, 11 Mar 2025 12:47:12 +0100 Subject: [PATCH 7/7] Rust: Update regex injection description Co-authored-by: Geoffrey White <40627776+geoffw0@users.noreply.github.com> --- rust/ql/src/queries/security/CWE-020/RegexInjection.qhelp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust/ql/src/queries/security/CWE-020/RegexInjection.qhelp b/rust/ql/src/queries/security/CWE-020/RegexInjection.qhelp index f314de2042e2..eb69a607954b 100644 --- a/rust/ql/src/queries/security/CWE-020/RegexInjection.qhelp +++ b/rust/ql/src/queries/security/CWE-020/RegexInjection.qhelp @@ -42,7 +42,7 @@ unexpectedly cause strings such as "key=secret" to match.

    If user input is used to construct a regular expression, it should be escaped -first. This ensures that the malicious users cannot insert characters that have special +first. This ensures that malicious users cannot insert characters that have special meanings in regular expressions.