Skip to content

Commit 8223dde

Browse files
committed
Rust: TaintedPath query
1 parent 2750d1d commit 8223dde

File tree

6 files changed

+181
-0
lines changed

6 files changed

+181
-0
lines changed
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/** Provides classes and predicates to reason about path injection vulnerabilities. */
2+
3+
import rust
4+
private import codeql.rust.controlflow.BasicBlocks
5+
private import codeql.rust.controlflow.ControlFlowGraph
6+
private import codeql.rust.dataflow.DataFlow
7+
private import codeql.rust.dataflow.TaintTracking
8+
private import codeql.rust.Concepts
9+
private import codeql.rust.dataflow.internal.DataFlowImpl
10+
11+
/**
12+
* Provides default sources, sinks and barriers for detecting path injection
13+
* vulnerabilities, as well as extension points for adding your own.
14+
*/
15+
module TaintedPath {
16+
/**
17+
* A data flow source for path injection vulnerabilities.
18+
*/
19+
abstract class Source extends DataFlow::Node { }
20+
21+
/**
22+
* A data flow sink for path injection vulnerabilities.
23+
*/
24+
abstract class Sink extends DataFlow::Node { }
25+
26+
/**
27+
* A barrier for path injection vulnerabilities.
28+
*/
29+
abstract class Barrier extends DataFlow::Node { }
30+
31+
/**
32+
* An active threat-model source, considered as a flow source.
33+
*/
34+
private class ActiveThreatModelSourceAsSource extends Source, ActiveThreatModelSource { }
35+
36+
/** A sink for path-injection from model data. */
37+
private class ModelsAsDataSinks extends Sink {
38+
ModelsAsDataSinks() { sinkNode(this, "path-injection") }
39+
}
40+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<!DOCTYPE qhelp PUBLIC
2+
"-//Semmle//qhelp//EN"
3+
"qhelp.dtd">
4+
<qhelp>
5+
<overview>
6+
<p>Accessing paths controlled by users can allow an attacker to access unexpected resources. This
7+
can result in sensitive information being revealed or deleted, or an attacker being able to influence
8+
behavior by modifying unexpected files.</p>
9+
10+
<p>Paths that are naively constructed from data controlled by a user may be absolute paths, or may contain
11+
unexpected special characters such as "..". Such a path could point anywhere on the file system.</p>
12+
13+
</overview>
14+
<recommendation>
15+
16+
<p>Validate user input before using it to construct a file path.</p>
17+
18+
<p>Common validation methods include checking that the normalized path is relative and does not contain
19+
any ".." components, or checking that the path is contained within a safe folder. The method you should use depends
20+
on how the path is used in the application, and whether the path should be a single path component.
21+
</p>
22+
23+
<p>If the path should be a single path component (such as a file name), you can check for the existence
24+
of any path separators ("/" or "\"), or ".." sequences in the input, and reject the input if any are found.
25+
</p>
26+
27+
<p>
28+
Note that removing "../" sequences is <i>not</i> sufficient, since the input could still contain a path separator
29+
followed by "..". For example, the input ".../...//" would still result in the string "../" if only "../" sequences
30+
are removed.
31+
</p>
32+
33+
<p>Finally, the simplest (but most restrictive) option is to use an allow list of safe patterns and make sure that
34+
the user input matches one of these patterns.</p>
35+
36+
</recommendation>
37+
<example>
38+
39+
<p>In this example, a user-provided file name is read from a HTTP request and then used to access a file
40+
and send it back to the user. However, a malicious user could enter a file name anywhere on the file system,
41+
such as "/etc/passwd" or "../../../etc/passwd".</p>
42+
43+
<sample src="examples/TaintedPath.rs" />
44+
45+
<p>
46+
If the input should only be a file name, you can check that it doesn't contain any path separators or ".." sequences.
47+
</p>
48+
49+
<sample src="examples/TaintedPathGoodNormalize.rs" />
50+
51+
<p>
52+
If the input should be within a specific directory, you can check that the resolved path
53+
is still contained within that directory.
54+
</p>
55+
56+
<sample src="examples/TaintedPathGoodFolder.rs" />
57+
58+
</example>
59+
<references>
60+
61+
<li>
62+
OWASP:
63+
<a href="https://owasp.org/www-community/attacks/Path_Traversal">Path Traversal</a>.
64+
</li>
65+
66+
</references>
67+
</qhelp>
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/**
2+
* @name Uncontrolled data used in path expression
3+
* @description Accessing paths influenced by users can allow an attacker to access unexpected resources.
4+
* @kind path-problem
5+
* @problem.severity error
6+
* @security-severity 7.5
7+
* @precision high
8+
* @id rust/path-injection
9+
* @tags security
10+
* external/cwe/cwe-022
11+
* external/cwe/cwe-023
12+
* external/cwe/cwe-036
13+
* external/cwe/cwe-073
14+
* external/cwe/cwe-099
15+
*/
16+
17+
import rust
18+
import codeql.rust.dataflow.DataFlow
19+
import codeql.rust.dataflow.TaintTracking
20+
import codeql.rust.security.TaintedPathExtensions
21+
import TaintedPathFlow::PathGraph
22+
23+
/**
24+
* A taint configuration for tainted data that reaches a file access sink.
25+
*/
26+
module TaintedPathConfig implements DataFlow::ConfigSig {
27+
predicate isSource(DataFlow::Node node) { node instanceof TaintedPath::Source }
28+
29+
predicate isSink(DataFlow::Node node) { node instanceof TaintedPath::Sink }
30+
31+
predicate isBarrier(DataFlow::Node barrier) { barrier instanceof TaintedPath::Barrier }
32+
}
33+
34+
module TaintedPathFlow = TaintTracking::Global<TaintedPathConfig>;
35+
36+
from TaintedPathFlow::PathNode source, TaintedPathFlow::PathNode sink
37+
where TaintedPathFlow::flowPath(source, sink)
38+
select sink.getNode(), source, sink, "This path depends on a $@.", source.getNode(),
39+
"user-provided value"
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
use poem::{error::InternalServerError, handler, web::Query, Result};
2+
use std::{fs, path::PathBuf};
3+
4+
#[handler]
5+
fn tainted_path_handler(Query(file_name): Query<String>) -> Result<String> {
6+
let file_path = PathBuf::from(file_name);
7+
// BAD: This could read any file on the filesystem.
8+
fs::read_to_string(file_path).map_err(InternalServerError)
9+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
use poem::{error::InternalServerError, handler, http::StatusCode, web::Query, Error, Result};
2+
use std::{env::home_dir, fs, path::PathBuf};
3+
4+
#[handler]
5+
fn tainted_path_handler(Query(file_path): Query<String>) -> Result<String, Error> {
6+
let public_path = home_dir().unwrap().join("public");
7+
let file_path = public_path.join(PathBuf::from(file_path));
8+
let file_path = file_path.canonicalize().unwrap();
9+
// GOOD: ensure that the path stays within the public folder
10+
if !file_path.starts_with(public_path) {
11+
return Err(Error::from_status(StatusCode::BAD_REQUEST));
12+
}
13+
fs::read_to_string(file_path).map_err(InternalServerError)
14+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
use poem::{error::InternalServerError, handler, http::StatusCode, web::Query, Error, Result};
2+
use std::{fs, path::PathBuf};
3+
4+
#[handler]
5+
fn tainted_path_handler(Query(file_name): Query<String>) -> Result<String> {
6+
// GOOD: ensure that the filename has no path separators or parent directory references
7+
if file_name.contains("..") || file_name.contains("/") || file_name.contains("\\") {
8+
return Err(Error::from_status(StatusCode::BAD_REQUEST));
9+
}
10+
let file_path = PathBuf::from(file_name);
11+
fs::read_to_string(file_path).map_err(InternalServerError)
12+
}

0 commit comments

Comments
 (0)