Skip to content
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

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open

Rust: TaintedPath query #18960

wants to merge 13 commits into from

Conversation

aibaars
Copy link
Contributor

@aibaars aibaars commented Mar 10, 2025

No description provided.

@github-actions github-actions bot added documentation Rust Pull requests that update Rust code labels Mar 10, 2025
Copy link
Contributor

github-actions bot commented Mar 10, 2025

QHelp previews:

rust/ql/src/queries/security/CWE-022/TaintedPath.qhelp

Uncontrolled data used in path expression

Accessing 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.

Recommendation

Validate 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.

Example

In 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

@aibaars aibaars force-pushed the aibaars/rust-tainted-path branch 5 times, most recently from 8890b12 to 4bf8103 Compare March 13, 2025 17:54
@aibaars aibaars marked this pull request as ready for review March 13, 2025 17:54
@Copilot Copilot bot review requested due to automatic review settings March 13, 2025 17:54

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()
Copy link
Contributor

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@aibaars aibaars force-pushed the aibaars/rust-tainted-path branch 2 times, most recently from 11510a9 to 2daef7e Compare March 14, 2025 10:29
Copy link
Contributor

@geoffw0 geoffw0 left a 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.

@aibaars
Copy link
Contributor Author

aibaars commented Mar 18, 2025

@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"]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- ["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.

Copy link
Contributor Author

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.

Copy link
Contributor

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.

Copy link
Contributor Author

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

Comment on lines 1865 to 1868
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())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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.

@aibaars aibaars force-pushed the aibaars/rust-tainted-path branch from 586759f to 0724151 Compare March 18, 2025 18:46
@aibaars aibaars requested a review from a team as a code owner March 18, 2025 18:46
Comment on lines +1136 to +1140
/**
* 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

The QLDoc for a predicate without a result should start with 'Holds'.
Comment on lines +1149 to +1150
/**
* 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

The QLDoc for a predicate without a result should start with 'Holds'.
@yoff
Copy link
Contributor

yoff commented Mar 19, 2025

Python 👍

@aibaars aibaars added the no-change-note-required This PR does not need a change note label Mar 20, 2025
@aibaars aibaars force-pushed the aibaars/rust-tainted-path branch from 0724151 to 2804c13 Compare March 20, 2025 10:37
@@ -1105,3 +1130,29 @@ private module Cached {
}

import Cached

cached
private module OptionalSteps {
Copy link
Contributor

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.

@aibaars aibaars force-pushed the aibaars/rust-tainted-path branch from e3761c8 to b10a296 Compare March 20, 2025 15:49
@geoffw0 geoffw0 added the ready-for-doc-review This PR requires and is ready for review from the GitHub docs team. label Mar 21, 2025
@geoffw0
Copy link
Contributor

geoffw0 commented Mar 21, 2025

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 ready-for-doc-review tag to get that aspect moving. Let me know if there's anything I can do to help get this merged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
documentation no-change-note-required This PR does not need a change note Python ready-for-doc-review This PR requires and is ready for review from the GitHub docs team. Rust Pull requests that update Rust code
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants