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

Merged
merged 14 commits into from
Mar 27, 2025
Merged

Rust: TaintedPath query #18960

merged 14 commits into from
Mar 27, 2025

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

@Copilot Copilot AI left a 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

@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/ .

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

@mchammer01 mchammer01 self-requested a review March 24, 2025 15:56
mchammer01
mchammer01 previously approved these changes Mar 24, 2025
Copy link
Contributor

@mchammer01 mchammer01 left a comment

Choose a reason for hiding this comment

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

@aibaars - Approving on behalf of Docs ⚡
I've left a couple of minor comments due to the repetition of the same verb in the same sentence. Would be good to find a synonym, but this can be merged with the sentences as they are 😅

"qhelp.dtd">
<qhelp>
<overview>
<p>Accessing paths controlled by users can allow an attacker to access unexpected resources. This
Copy link
Contributor

Choose a reason for hiding this comment

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

This LGTM, just highlighting that we're using the verb "to access" twice in this sentence. Perhaps we could use a synonym to avoid repetition?
Not 100% happy with my suggestion though 🤔

Suggested change
<p>Accessing paths controlled by users can allow an attacker to access unexpected resources. This
<p>Accessing paths controlled by users can allow an attacker to reach unexpected resources. This

Copy link
Contributor Author

@aibaars aibaars Mar 25, 2025

Choose a reason for hiding this comment

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

I agree with the comment, but I'm not 100% sold on the suggestion either. I just checked and the 7 other languages use the same phrase with the double use of "access". So we should either change all 8 QL and QHelp files consistently or none.

If you feel strongly about the need to avoid the double use of access then I'd be happy to address this in a separate pull request for all languages.

I don't really like "reach", it is even more vague than "access" . A clearer phrase would be ... to read, write or modify unexpected resources, but that is perhaps a bit too long.

Copy link
Contributor

Choose a reason for hiding this comment

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

Let's leave the verbiage as-is, if it's also used in other queries 👍🏻 😅

@@ -0,0 +1,99 @@
/**
* @name Uncontrolled data used in path expression
* @description Accessing paths influenced by users can allow an attacker to access unexpected resources.
Copy link
Contributor

Choose a reason for hiding this comment

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

Same comment as above.

Copy link
Contributor

@hvitved hvitved left a comment

Choose a reason for hiding this comment

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

A few minor suggestions, otherwise LGTM

Comment on lines 272 to 279
/**
* A data-flow node that performs path normalization. This is often needed in order
* to safely access paths.
*/
class PathNormalization extends DataFlow::Node instanceof PathNormalization::Range {
/** Gets an argument to this path normalization that is interpreted as a path. */
DataFlow::Node getPathArg() { result = super.getPathArg() }
}
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
/**
* A data-flow node that performs path normalization. This is often needed in order
* to safely access paths.
*/
class PathNormalization extends DataFlow::Node instanceof PathNormalization::Range {
/** Gets an argument to this path normalization that is interpreted as a path. */
DataFlow::Node getPathArg() { result = super.getPathArg() }
}
final class PathNormalization = PathNormalization::Range;

* be treated as an ordinary content component.
*/
cached
predicate isSpecialContentSet(ContentSet cs) {
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 this can be private and non-cached?

import TaintedPathFlow::PathGraph
private import codeql.rust.Concepts

abstract private class NormalizationState extends string {
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: I think it would be cleaner to use a newtype to represent the state.

@@ -0,0 +1,3 @@
qltest_cargo_check: true
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think this line is needed, it should be inherited from rust/ql/test/options.yml?

@aibaars aibaars force-pushed the aibaars/rust-tainted-path branch from cca001a to bf76505 Compare March 26, 2025 12:29
@aibaars aibaars merged commit 9dd7b20 into main Mar 27, 2025
24 checks passed
@aibaars aibaars deleted the aibaars/rust-tainted-path branch March 27, 2025 09:37
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.

7 participants