mirror of
https://github.com/github/codeql.git
synced 2026-04-25 16:55:19 +02:00
Merge pull request #18946 from paldepind/rust-regex-injection
Rust: Add regular expression injection query
This commit is contained in:
@@ -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 |
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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 |
|
||||
|
||||
7
rust/ql/lib/codeql/rust/frameworks/regex.model.yml
Normal file
7
rust/ql/lib/codeql/rust/frameworks/regex.model.yml
Normal file
@@ -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"]
|
||||
@@ -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() = "<crate::regex::string::Regex>::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"
|
||||
}
|
||||
}
|
||||
56
rust/ql/src/queries/security/CWE-020/RegexInjection.qhelp
Normal file
56
rust/ql/src/queries/security/CWE-020/RegexInjection.qhelp
Normal file
@@ -0,0 +1,56 @@
|
||||
<!DOCTYPE qhelp PUBLIC
|
||||
"-//Semmle//qhelp//EN"
|
||||
"qhelp.dtd">
|
||||
<qhelp>
|
||||
|
||||
<overview>
|
||||
<p>
|
||||
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 construct large regular expressions by using
|
||||
counted repetitions.
|
||||
</p>
|
||||
</overview>
|
||||
|
||||
<recommendation>
|
||||
<p>
|
||||
Before embedding user input into a regular expression, escape the input string
|
||||
using a function such as <a
|
||||
href="https://docs.rs/regex/latest/regex/fn.escape.html">regex::escape</a> to
|
||||
escape meta-characters that have special meaning.
|
||||
</p>
|
||||
<p>
|
||||
If purposefully supporting user supplied regular expressions, then use <a
|
||||
href="https://docs.rs/regex/latest/regex/struct.RegexBuilder.html#method.size_limit">RegexBuilder::size_limit</a>
|
||||
to limit the pattern size so that it is no larger than necessary.
|
||||
</p>
|
||||
</recommendation>
|
||||
|
||||
<example>
|
||||
<p>
|
||||
The following example constructs a regular expressions from the user input
|
||||
<code>key</code> without escaping it first.
|
||||
</p>
|
||||
|
||||
<sample src="RegexInjectionBad.rs" />
|
||||
|
||||
<p>
|
||||
The regular expression is intended to match strings starting with
|
||||
<code>"property"</code> such as <code>"property:foo=bar"</code>. However, a
|
||||
malicious user might inject the regular expression <code>".*^|key"</code> and
|
||||
unexpectedly cause strings such as <code>"key=secret"</code> to match.
|
||||
</p>
|
||||
<p>
|
||||
If user input is used to construct a regular expression, it should be escaped
|
||||
first. This ensures that malicious users cannot insert characters that have special
|
||||
meanings in regular expressions.
|
||||
</p>
|
||||
<sample src="RegexInjectionGood.rs" />
|
||||
</example>
|
||||
|
||||
<references>
|
||||
<li>
|
||||
<code>regex</code> crate documentation: <a href="https://docs.rs/regex/latest/regex/index.html#untrusted-patterns">Untrusted patterns</a>.
|
||||
</li>
|
||||
</references>
|
||||
</qhelp>
|
||||
48
rust/ql/src/queries/security/CWE-020/RegexInjection.ql
Normal file
48
rust/ql/src/queries/security/CWE-020/RegexInjection.ql
Normal file
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* @name Regular expression injection
|
||||
* @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
|
||||
* @precision high
|
||||
* @id rust/regex-injection
|
||||
* @tags security
|
||||
* external/cwe/cwe-020
|
||||
* external/cwe/cwe-074
|
||||
*/
|
||||
|
||||
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 ActiveThreatModelSource }
|
||||
|
||||
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<RegexInjectionConfig>;
|
||||
|
||||
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"
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
6
rust/ql/test/default-threat-models.model.yml
Normal file
6
rust/ql/test/default-threat-models.model.yml
Normal file
@@ -0,0 +1,6 @@
|
||||
extensions:
|
||||
- addsTo:
|
||||
pack: codeql/threat-models
|
||||
extensible: threatModelConfiguration
|
||||
data:
|
||||
- ["local", true, 0]
|
||||
@@ -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::_::<crate::vec::into_iter::IntoIter as crate::iter::traits::iterator::Iterator>::fold | function return | file://:0:0:0:0 | [summary] read: Argument[1].ReturnValue in lang:alloc::_::<crate::vec::into_iter::IntoIter as crate::iter::traits::iterator::Iterator>::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 |
|
||||
|
||||
@@ -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). |
|
||||
|
||||
@@ -6,3 +6,5 @@ dependencies:
|
||||
extractor: rust
|
||||
tests: .
|
||||
warnOnImplicitThis: true
|
||||
dataExtensions:
|
||||
- default-threat-models.model.yml
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,2 @@
|
||||
query: queries/security/CWE-020/RegexInjection.ql
|
||||
postprocess: utils/test/InlineExpectationsTestQuery.ql
|
||||
@@ -0,0 +1,2 @@
|
||||
| main.rs:6:25:6:30 | ®ex |
|
||||
| main.rs:14:25:14:30 | ®ex |
|
||||
@@ -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 }
|
||||
36
rust/ql/test/query-tests/security/CWE-020/main.rs
Normal file
36
rust/ql/test/query-tests/security/CWE-020/main.rs
Normal file
@@ -0,0 +1,36 @@
|
||||
use regex::Regex;
|
||||
|
||||
fn simple_bad(hay: &str) -> Option<bool> {
|
||||
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<bool> {
|
||||
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<bool> {
|
||||
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<bool> {
|
||||
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();
|
||||
}
|
||||
3
rust/ql/test/query-tests/security/CWE-020/options.yml
Normal file
3
rust/ql/test/query-tests/security/CWE-020/options.yml
Normal file
@@ -0,0 +1,3 @@
|
||||
qltest_cargo_check: true
|
||||
qltest_dependencies:
|
||||
- regex = { version = "1.11.1" }
|
||||
@@ -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; <crate::string::String>::as_str; Argument[self]; ReturnValue; taint |
|
||||
| 3 | Summary: lang:alloc; crate::fmt::format; Argument[0]; ReturnValue; taint |
|
||||
| 4 | Summary: lang:core; <crate::result::Result>::unwrap; Argument[self].Field[crate::result::Result::Ok(0)]; ReturnValue; value |
|
||||
| 5 | Summary: lang:core; <crate::result::Result>::unwrap_or; Argument[self].Field[crate::result::Result::Ok(0)]; ReturnValue; value |
|
||||
| 6 | Summary: lang:core; <str>::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; <crate::blocking::response::Response>::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; <crate::string::String>::as_str; Argument[self]; ReturnValue; taint |
|
||||
| 4 | Summary: lang:alloc; crate::fmt::format; Argument[0]; ReturnValue; taint |
|
||||
| 5 | Summary: lang:core; <crate::option::Option>::unwrap_or; Argument[self].Field[crate::option::Option::Some(0)]; ReturnValue; value |
|
||||
| 6 | Summary: lang:core; <crate::result::Result>::unwrap; Argument[self].Field[crate::result::Result::Ok(0)]; ReturnValue; value |
|
||||
| 7 | Summary: lang:core; <crate::result::Result>::unwrap_or; Argument[self].Field[crate::result::Result::Ok(0)]; ReturnValue; value |
|
||||
| 8 | Summary: lang:core; <str>::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; <crate::blocking::response::Response>::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(...) |
|
||||
|
||||
@@ -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::<i32>().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
|
||||
|
||||
Reference in New Issue
Block a user