Skip to content

Commit 3995825

Browse files
committed
Rust: Add command injection query (CWE-078)
Detects user-controlled data flowing into std::process::Command and tokio::process::Command (both command name and arguments). - Extension library with sources, sinks, and barriers - Models-as-data sinks for Command::new, .arg(), .args() (std + tokio) - Query help (.qhelp) with examples - Test cases with inline expectations Query ID: rust/command-line-injection
1 parent 08547cb commit 3995825

9 files changed

Lines changed: 267 additions & 0 deletions

File tree

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
extensions:
2+
- addsTo:
3+
pack: codeql/rust-all
4+
extensible: sinkModel
5+
data:
6+
# std::process::Command - the command name itself
7+
- ["<std::process::Command>::new", "Argument[0]", "command-injection", "manual"]
8+
# std::process::Command - arguments passed to the command
9+
- ["<std::process::Command>::arg", "Argument[0]", "command-injection", "manual"]
10+
- ["<std::process::Command>::args", "Argument[0]", "command-injection", "manual"]
11+
# tokio::process::Command - the command name itself
12+
- ["<tokio::process::Command>::new", "Argument[0]", "command-injection", "manual"]
13+
# tokio::process::Command - arguments passed to the command
14+
- ["<tokio::process::Command>::arg", "Argument[0]", "command-injection", "manual"]
15+
- ["<tokio::process::Command>::args", "Argument[0]", "command-injection", "manual"]
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/**
2+
* Provides classes and predicates for reasoning about command injection
3+
* vulnerabilities (CWE-078).
4+
*/
5+
6+
import rust
7+
private import codeql.rust.dataflow.DataFlow
8+
private import codeql.rust.dataflow.FlowSink
9+
private import codeql.rust.dataflow.FlowBarrier
10+
private import codeql.rust.Concepts
11+
private import codeql.rust.security.Barriers as Barriers
12+
13+
/**
14+
* Provides default sources, sinks and barriers for detecting command injection
15+
* vulnerabilities, as well as extension points for adding your own.
16+
*/
17+
module CommandInjection {
18+
/**
19+
* A data flow source for command injection vulnerabilities.
20+
*/
21+
abstract class Source extends DataFlow::Node { }
22+
23+
/**
24+
* A data flow sink for command injection vulnerabilities.
25+
*/
26+
abstract class Sink extends QuerySink::Range {
27+
override string getSinkType() { result = "CommandInjection" }
28+
}
29+
30+
/**
31+
* A barrier for command injection vulnerabilities.
32+
*/
33+
abstract class Barrier extends DataFlow::Node { }
34+
35+
/**
36+
* An active threat-model source, considered as a flow source.
37+
*/
38+
private class ActiveThreatModelSourceAsSource extends Source, ActiveThreatModelSource { }
39+
40+
/**
41+
* A sink for command injection from model data.
42+
*/
43+
private class ModelsAsDataSink extends Sink {
44+
ModelsAsDataSink() { sinkNode(this, "command-injection") }
45+
}
46+
47+
/**
48+
* A barrier for command injection from model data.
49+
*/
50+
private class ModelsAsDataBarrier extends Barrier {
51+
ModelsAsDataBarrier() { barrierNode(this, "command-injection") }
52+
}
53+
54+
/**
55+
* A barrier for command injection vulnerabilities for nodes whose type is a
56+
* numeric type, which is unlikely to expose any vulnerability.
57+
*/
58+
private class NumericTypeBarrier extends Barrier instanceof Barriers::NumericTypeBarrier { }
59+
60+
private class BooleanTypeBarrier extends Barrier instanceof Barriers::BooleanTypeBarrier { }
61+
62+
private class FieldlessEnumTypeBarrier extends Barrier instanceof Barriers::FieldlessEnumTypeBarrier
63+
{ }
64+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<!DOCTYPE qhelp PUBLIC
2+
"-//Semmle//qhelp//EN"
3+
"qhelp.dtd">
4+
<qhelp>
5+
<overview>
6+
7+
<p>
8+
If a system command is built from user-provided data without sufficient sanitization, a user may be able to run malicious commands. An attacker can craft input to change the meaning of the command, potentially gaining control of the system.
9+
</p>
10+
11+
</overview>
12+
<recommendation>
13+
14+
<p>
15+
If possible, use hard-coded string literals for commands. If the command must be built from user-provided data, do not pass user input directly to shell commands. Instead, use APIs that accept command arguments as separate parameters (such as <code>std::process::Command</code> with individual <code>.arg()</code> calls for each argument), which avoids shell interpretation of special characters. If shell execution is necessary, validate and sanitize user input against an allowlist of permitted values.
16+
</p>
17+
18+
</recommendation>
19+
<example>
20+
21+
<p>
22+
In the following example, a command is constructed directly from user-controlled input obtained via an HTTP request. An attacker could supply a malicious value to execute arbitrary commands.
23+
</p>
24+
25+
<sample src="CommandInjectionBad.rs" />
26+
27+
<p>
28+
A safer approach uses a fixed command with validated arguments, or avoids shell interpretation entirely:
29+
</p>
30+
31+
<sample src="CommandInjectionGood.rs" />
32+
33+
</example>
34+
<references>
35+
36+
<li>OWASP: <a href="https://owasp.org/www-community/attacks/Command_Injection">Command Injection</a>.</li>
37+
<li>Wikipedia: <a href="https://en.wikipedia.org/wiki/Code_injection#Shell_injection">Shell injection</a>.</li>
38+
39+
</references>
40+
</qhelp>
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/**
2+
* @name Uncontrolled command line
3+
* @description Using externally controlled strings in a command line may allow a malicious
4+
* user to change the meaning of the command.
5+
* @kind path-problem
6+
* @problem.severity error
7+
* @security-severity 9.8
8+
* @precision high
9+
* @id rust/command-line-injection
10+
* @tags security
11+
* external/cwe/cwe-078
12+
* external/cwe/cwe-088
13+
*/
14+
15+
import rust
16+
import codeql.rust.dataflow.DataFlow
17+
import codeql.rust.dataflow.TaintTracking
18+
import codeql.rust.security.CommandInjectionExtensions
19+
20+
/**
21+
* A taint configuration for detecting command injection vulnerabilities.
22+
*/
23+
module CommandInjectionConfig implements DataFlow::ConfigSig {
24+
import CommandInjection
25+
26+
predicate isSource(DataFlow::Node node) { node instanceof Source }
27+
28+
predicate isSink(DataFlow::Node node) { node instanceof Sink }
29+
30+
predicate isBarrier(DataFlow::Node barrier) { barrier instanceof Barrier }
31+
32+
predicate observeDiffInformedIncrementalMode() { any() }
33+
}
34+
35+
module CommandInjectionFlow = TaintTracking::Global<CommandInjectionConfig>;
36+
37+
import CommandInjectionFlow::PathGraph
38+
39+
from CommandInjectionFlow::PathNode sourceNode, CommandInjectionFlow::PathNode sinkNode
40+
where CommandInjectionFlow::flowPath(sourceNode, sinkNode)
41+
select sinkNode.getNode(), sourceNode, sinkNode, "This command line depends on a $@.",
42+
sourceNode.getNode(), "user-provided value"
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
use std::process::Command;
2+
3+
fn handle_request(user_input: &str) {
4+
// BAD: user input is passed directly to a shell command
5+
Command::new("sh")
6+
.arg("-c")
7+
.arg(user_input)
8+
.output()
9+
.expect("failed to execute");
10+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
use std::process::Command;
2+
3+
fn handle_request(filename: &str) {
4+
// GOOD: use a fixed command with the user input as a separate argument,
5+
// avoiding shell interpretation
6+
let allowed_names = ["report.pdf", "summary.txt", "data.csv"];
7+
if allowed_names.contains(&filename) {
8+
Command::new("cat")
9+
.arg(filename)
10+
.output()
11+
.expect("failed to execute");
12+
}
13+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
query: queries/security/CWE-078/CommandInjection.ql
2+
postprocess:
3+
- utils/test/PrettyPrintModels.ql
4+
- utils/test/InlineExpectationsTestQuery.ql
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
use std::process::Command;
2+
3+
fn test_std_command_injection() {
4+
let arg_string = std::env::args().nth(1).unwrap_or(String::from("ls")); // $ Source=args1
5+
let remote_string = reqwest::blocking::get("http://example.com/") // $ Source=remote1
6+
.unwrap()
7+
.text()
8+
.unwrap_or(String::from("ls"));
9+
let const_string = String::from("echo hello");
10+
11+
// --- safe cases ---
12+
13+
// Constant command and argument
14+
Command::new("ls")
15+
.arg("-la")
16+
.output()
17+
.expect("failed"); // safe
18+
19+
// Constant constructed command
20+
Command::new(const_string.as_str())
21+
.output()
22+
.expect("failed"); // safe
23+
24+
// --- unsafe cases ---
25+
26+
// User input as the command itself
27+
Command::new(arg_string.as_str()) // $ Alert[rust/command-line-injection]=args1
28+
.output()
29+
.expect("failed");
30+
31+
// User input as an argument to sh -c
32+
Command::new("sh")
33+
.arg("-c")
34+
.arg(remote_string.as_str()) // $ Alert[rust/command-line-injection]=remote1
35+
.output()
36+
.expect("failed");
37+
38+
// User input as an argument
39+
Command::new("grep")
40+
.arg(arg_string.as_str()) // $ Alert[rust/command-line-injection]=args1
41+
.arg("file.txt")
42+
.output()
43+
.expect("failed");
44+
45+
// Remote input via args()
46+
Command::new("bash")
47+
.args(&["-c", remote_string.as_str()]) // $ Alert[rust/command-line-injection]=remote1
48+
.output()
49+
.expect("failed");
50+
}
51+
52+
async fn test_tokio_command_injection() {
53+
let remote_string = reqwest::blocking::get("http://example.com/") // $ Source=remote2
54+
.unwrap()
55+
.text()
56+
.unwrap_or(String::from("ls"));
57+
58+
// Unsafe: remote input as tokio command
59+
let _output = tokio::process::Command::new(remote_string.as_str()) // $ Alert[rust/command-line-injection]=remote2
60+
.output()
61+
.await
62+
.expect("failed");
63+
64+
// Unsafe: remote input as tokio command argument
65+
tokio::process::Command::new("sh")
66+
.arg("-c")
67+
.arg(remote_string.as_str()) // $ Alert[rust/command-line-injection]=remote2
68+
.output()
69+
.await
70+
.expect("failed");
71+
}
72+
73+
fn main() {
74+
test_std_command_injection();
75+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
qltest_cargo_check: true
2+
qltest_dependencies:
3+
- reqwest = { version = "0.12.9", features = ["blocking"] }
4+
- tokio = { version = "1", features = ["full"] }

0 commit comments

Comments
 (0)