-
Notifications
You must be signed in to change notification settings - Fork 1.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Rust: TaintedPath query #18960
base: main
Are you sure you want to change the base?
Rust: TaintedPath query #18960
Conversation
QHelp previews: rust/ql/src/queries/security/CWE-022/TaintedPath.qhelpUncontrolled data used in path expressionAccessing paths controlled by users can allow an attacker to access unexpected resources. This can result in sensitive information being revealed or deleted, or an attacker being able to influence behavior by modifying unexpected files. Paths that are naively constructed from data controlled by a user may be absolute paths, or may contain unexpected special characters such as "..". Such a path could point anywhere on the file system. RecommendationValidate user input before using it to construct a file path. Common validation methods include checking that the normalized path is relative and does not contain any ".." components, or checking that the path is contained within a safe folder. The method you should use depends on how the path is used in the application, and whether the path should be a single path component. If the path should be a single path component (such as a file name), you can check for the existence of any path separators ("/" or "\"), or ".." sequences in the input, and reject the input if any are found. Note that removing "../" sequences is not sufficient, since the input could still contain a path separator followed by "..". For example, the input ".../...//" would still result in the string "../" if only "../" sequences are removed. Finally, the simplest (but most restrictive) option is to use an allow list of safe patterns and make sure that the user input matches one of these patterns. ExampleIn this example, a user-provided file name is read from a HTTP request and then used to access a file and send it back to the user. However, a malicious user could enter a file name anywhere on the file system, such as "/etc/passwd" or "../../../etc/passwd". use poem::{error::InternalServerError, handler, web::Query, Result};
use std::{fs, path::PathBuf};
#[handler]
fn tainted_path_handler(Query(file_name): Query<String>) -> Result<String> {
let file_path = PathBuf::from(file_name);
// BAD: This could read any file on the filesystem.
fs::read_to_string(file_path).map_err(InternalServerError)
} If the input should only be a file name, you can check that it doesn't contain any path separators or ".." sequences. use poem::{error::InternalServerError, handler, http::StatusCode, web::Query, Error, Result};
use std::{fs, path::PathBuf};
#[handler]
fn tainted_path_handler(Query(file_name): Query<String>) -> Result<String> {
// GOOD: ensure that the filename has no path separators or parent directory references
if file_name.contains("..") || file_name.contains("/") || file_name.contains("\\") {
return Err(Error::from_status(StatusCode::BAD_REQUEST));
}
let file_path = PathBuf::from(file_name);
fs::read_to_string(file_path).map_err(InternalServerError)
} If the input should be within a specific directory, you can check that the resolved path is still contained within that directory. use poem::{error::InternalServerError, handler, http::StatusCode, web::Query, Error, Result};
use std::{env::home_dir, fs, path::PathBuf};
#[handler]
fn tainted_path_handler(Query(file_path): Query<String>) -> Result<String, Error> {
let public_path = home_dir().unwrap().join("public");
let file_path = public_path.join(PathBuf::from(file_path));
let file_path = file_path.canonicalize().unwrap();
// GOOD: ensure that the path stays within the public folder
if !file_path.starts_with(public_path) {
return Err(Error::from_status(StatusCode::BAD_REQUEST));
}
fs::read_to_string(file_path).map_err(InternalServerError)
} References |
8890b12
to
4bf8103
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This pull request adds examples demonstrating both secure and insecure handling of file path tainting in Rust.
- Introduces a secure example that validates a file name against path traversal in TaintedPathGoodNormalize.rs.
- Implements a secure example that ensures file access remains within a designated public folder in TaintedPathGoodFolder.rs.
- Provides an insecure example in TaintedPath.rs to illustrate unsafe file reading.
- Updates model files to include new taint propagation rules.
Reviewed Changes
Copilot reviewed 9 out of 23 changed files in this pull request and generated no comments.
Show a summary per file
File | Description |
---|---|
rust/ql/src/queries/security/CWE-022/examples/TaintedPathGoodNormalize.rs | Adds a secure file read example with filename validation. |
rust/ql/src/queries/security/CWE-022/examples/TaintedPathGoodFolder.rs | Adds a secure file read example ensuring file stays within a public folder. |
rust/ql/src/queries/security/CWE-022/examples/TaintedPath.rs | Adds an insecure file read example to demonstrate tainted file path usage. |
rust/ql/lib/codeql/rust/frameworks/stdlib/fs.model.yml | Updates the model to include path-injection taint propagation for fs::read_to_string. |
rust/ql/lib/codeql/rust/frameworks/stdlib/lang-core.model.yml | Adds a taint propagation rule for Result::unwrap. |
Files not reviewed (14)
- rust/ql/integration-tests/hello-project/summary.expected: Language not supported
- rust/ql/integration-tests/hello-workspace/summary.cargo.expected: Language not supported
- rust/ql/integration-tests/hello-workspace/summary.rust-project.expected: Language not supported
- rust/ql/lib/codeql/rust/Concepts.qll: Language not supported
- rust/ql/lib/codeql/rust/Frameworks.qll: Language not supported
- rust/ql/lib/codeql/rust/dataflow/DataFlow.qll: Language not supported
- rust/ql/lib/codeql/rust/dataflow/internal/SsaImpl.qll: Language not supported
- rust/ql/lib/codeql/rust/frameworks/Poem.qll: Language not supported
- rust/ql/lib/codeql/rust/frameworks/stdlib/Stdlib.qll: Language not supported
- rust/ql/lib/codeql/rust/security/TaintedPathExtensions.qll: Language not supported
- rust/ql/src/queries/security/CWE-022/TaintedPath.qhelp: Language not supported
- rust/ql/src/queries/security/CWE-022/TaintedPath.ql: Language not supported
- rust/ql/test/query-tests/security/CWE-022/TaintedPath.expected: Language not supported
- rust/ql/test/query-tests/security/CWE-022/TaintedPath.qlref: Language not supported
Comments suppressed due to low confidence (1)
rust/ql/src/queries/security/CWE-022/examples/TaintedPathGoodFolder.rs:7
- The variable 'file_path' shadows the function parameter; use a distinct variable name for clarity.
let file_path = public_path.join(PathBuf::from(file_path));
Tip: Copilot code review supports C#, Go, Java, JavaScript, Markdown, Python, Ruby and TypeScript, with more languages coming soon. Learn more
@@ -212,7 +212,8 @@ predicate capturedCallWrite(Expr call, BasicBlock bb, int i, Variable v) { | |||
/** Holds if `v` may be mutably borrowed in `e`. */ | |||
private predicate mutablyBorrows(Expr e, Variable v) { | |||
e = any(MethodCallExpr mc).getReceiver() and | |||
e.(VariableAccess).getVariable() = v | |||
e.(VariableAccess).getVariable() = v and | |||
v.isMutable() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it would be nice to factor this bit out into a separate PR, with its own DCA run.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
11510a9
to
2daef7e
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've asked some questions, query and qhelp otherwise LGTM. The examples in the .qhelp
are clear and helpful. 👍
Please do a DCA run and request a docs review when you're ready.
@geoffw0 The QHelp is exactly the same as C# and Java, the only difference are the examples which are meant to resemble the C#/Java variants as closely as possible. See for example: https://codeql.github.com/codeql-query-help/csharp/cs-path-injection/ . |
@@ -16,4 +16,5 @@ extensions: | |||
- ["lang:std", "<crate::path::PathBuf as crate::convert::From>::from", "Argument[0]", "ReturnValue", "taint", "manual"] | |||
- ["lang:std", "<crate::path::Path>::join", "Argument[self]", "ReturnValue", "taint", "manual"] | |||
- ["lang:std", "<crate::path::Path>::join", "Argument[0]", "ReturnValue", "taint", "manual"] | |||
- ["lang:std", "<crate::path::Path>::canonicalize", "Argument[self]", "ReturnValue.Field[crate::result::Result::Ok(0)].OptionalStep[normalize-path]", "taint", "manual"] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- ["lang:std", "<crate::path::Path>::canonicalize", "Argument[self]", "ReturnValue.Field[crate::result::Result::Ok(0)].OptionalStep[normalize-path]", "taint", "manual"] | |
- ["lang:std", "<crate::path::Path>::canonicalize", "Argument[self].OptionalStep[normalize-path]", "ReturnValue.Field[crate::result::Result::Ok(0)]", "taint", "manual"] |
Sorry, I put the optional step in the wrong place when I posted the code. It should be at the end of the input, not at the end of the output.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When I put the step in the "input" then I cannot get the isBarrier
predicate to work properly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you try with an OptionalBarrier
on the normal summary? Otherwise feel free to use the original form and I'll revisit this when building shared support for optional steps.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes that works, see also: 2804c13
FlowSummaryImpl::Private::Steps::summaryReadStep(node1.(Node::FlowSummaryNode).getSummaryNode(), | ||
TOptionalStep(name), node2.(Node::FlowSummaryNode).getSummaryNode()) or | ||
FlowSummaryImpl::Private::Steps::summaryStoreStep(node1.(Node::FlowSummaryNode).getSummaryNode(), | ||
TOptionalStep(name), node2.(Node::FlowSummaryNode).getSummaryNode()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FlowSummaryImpl::Private::Steps::summaryReadStep(node1.(Node::FlowSummaryNode).getSummaryNode(), | |
TOptionalStep(name), node2.(Node::FlowSummaryNode).getSummaryNode()) or | |
FlowSummaryImpl::Private::Steps::summaryStoreStep(node1.(Node::FlowSummaryNode).getSummaryNode(), | |
TOptionalStep(name), node2.(Node::FlowSummaryNode).getSummaryNode()) | |
FlowSummaryImpl::Private::Steps::summaryReadStep(node1.(Node::FlowSummaryNode).getSummaryNode(), | |
TOptionalStep(name), node2.(Node::FlowSummaryNode).getSummaryNode()) |
With the previous suggestion, the store step should not be necessary.
586759f
to
0724151
Compare
/** | ||
* A step in a flow summary defined using `OptionalStep[name]`. An `OptionalStep` is "opt-in", which means | ||
* that by default the step is not present in the flow summary and needs to be explicitly enabled by defining | ||
* an additional flow step. | ||
*/ |
Check warning
Code scanning / CodeQL
Predicate QLDoc style. Warning
/** | ||
* A step in a flow summary defined using `OptionalBarrier[name]`. An `OptionalBarrier` is "opt-out", by default | ||
* data can flow freely through the step. Flow through the step can be explicity blocked by defining its node as a barrier. | ||
*/ |
Check warning
Code scanning / CodeQL
Predicate QLDoc style. Warning
Python 👍 |
0724151
to
2804c13
Compare
@@ -1105,3 +1130,29 @@ private module Cached { | |||
} | |||
|
|||
import Cached | |||
|
|||
cached | |||
private module OptionalSteps { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be inside the existing Cached
module to make sure that they are evaluated together.
e3761c8
to
b10a296
Compare
All of my concerns have been addressed, but I don't speak for other people's comments. I've taken the liberty of adding the |
No description provided.