Merge pull request #18960 from github/aibaars/rust-tainted-path

Rust: TaintedPath query
This commit is contained in:
Arthur Baars
2025-03-27 10:37:36 +01:00
committed by GitHub
30 changed files with 707 additions and 13 deletions

View File

@@ -181,7 +181,7 @@ module Path {
}
}
/** A data-flow node that checks that a path is safe to access. */
/** A data-flow node that checks that a path is safe to access in some way, for example by having a controlled prefix. */
class SafeAccessCheck extends DataFlow::ExprNode {
SafeAccessCheck() { this = DataFlow::BarrierGuard<safeAccessCheck/3>::getABarrierNode() }
}
@@ -192,7 +192,7 @@ module Path {
/** Provides a class for modeling new path safety checks. */
module SafeAccessCheck {
/** A data-flow node that checks that a path is safe to access. */
/** A data-flow node that checks that a path is safe to access in some way, for example by having a controlled prefix. */
abstract class Range extends DataFlow::GuardNode {
/** Holds if this guard validates `node` upon evaluating to `branch`. */
abstract predicate checks(ControlFlowNode node, boolean branch);

View File

@@ -15,7 +15,7 @@
| Macro calls - resolved | 2 |
| Macro calls - total | 2 |
| Macro calls - unresolved | 0 |
| Taint edges - number of edges | 1671 |
| Taint edges - number of edges | 1674 |
| Taint reach - nodes tainted | 0 |
| Taint reach - per million nodes | 0 |
| Taint sinks - cryptographic operations | 0 |

View File

@@ -15,7 +15,7 @@
| Macro calls - resolved | 2 |
| Macro calls - total | 2 |
| Macro calls - unresolved | 0 |
| Taint edges - number of edges | 1671 |
| Taint edges - number of edges | 1674 |
| Taint reach - nodes tainted | 0 |
| Taint reach - per million nodes | 0 |
| Taint sinks - cryptographic operations | 0 |

View File

@@ -15,7 +15,7 @@
| Macro calls - resolved | 2 |
| Macro calls - total | 2 |
| Macro calls - unresolved | 0 |
| Taint edges - number of edges | 1671 |
| Taint edges - number of edges | 1674 |
| Taint reach - nodes tainted | 0 |
| Taint reach - per million nodes | 0 |
| Taint sinks - cryptographic operations | 0 |

View File

@@ -8,6 +8,8 @@ private import codeql.rust.dataflow.DataFlow
private import codeql.threatmodels.ThreatModels
private import codeql.rust.Frameworks
private import codeql.rust.dataflow.FlowSource
private import codeql.rust.controlflow.ControlFlowGraph as Cfg
private import codeql.rust.controlflow.CfgNodes as CfgNodes
/**
* A data flow source for a specific threat-model.
@@ -264,3 +266,38 @@ module Cryptography {
class CryptographicAlgorithm = SC::CryptographicAlgorithm;
}
/** Provides classes for modeling path-related APIs. */
module Path {
final class PathNormalization = PathNormalization::Range;
/** Provides a class for modeling new path normalization APIs. */
module PathNormalization {
/**
* A data-flow node that performs path normalization. This is often needed in order
* to safely access paths.
*/
abstract class Range extends DataFlow::Node {
/** Gets an argument to this path normalization that is interpreted as a path. */
abstract DataFlow::Node getPathArg();
}
}
/** A data-flow node that checks that a path is safe to access in some way, for example by having a controlled prefix. */
class SafeAccessCheck extends DataFlow::ExprNode {
SafeAccessCheck() { this = DataFlow::BarrierGuard<safeAccessCheck/3>::getABarrierNode() }
}
private predicate safeAccessCheck(CfgNodes::AstCfgNode g, Cfg::CfgNode node, boolean branch) {
g.(SafeAccessCheck::Range).checks(node, branch)
}
/** Provides a class for modeling new path safety checks. */
module SafeAccessCheck {
/** A data-flow node that checks that a path is safe to access in some way, for example by having a controlled prefix. */
abstract class Range extends CfgNodes::AstCfgNode {
/** Holds if this guard validates `node` upon evaluating to `branch`. */
abstract predicate checks(Cfg::CfgNode node, boolean branch);
}
}
}

View File

@@ -3,5 +3,7 @@
*/
private import codeql.rust.frameworks.rustcrypto.RustCrypto
private import codeql.rust.frameworks.Poem
private import codeql.rust.frameworks.Sqlx
private import codeql.rust.frameworks.stdlib.Clone
private import codeql.rust.frameworks.stdlib.Stdlib

View File

@@ -8,6 +8,8 @@ private import codeql.dataflow.DataFlow
private import internal.DataFlowImpl as DataFlowImpl
private import internal.Node as Node
private import internal.Content as Content
private import codeql.rust.controlflow.ControlFlowGraph as Cfg
private import codeql.rust.controlflow.CfgNodes as CfgNodes
/**
* Provides classes for performing local (intra-procedural) and global
@@ -16,6 +18,8 @@ private import internal.Content as Content
module DataFlow {
final class Node = Node::NodePublic;
final class ExprNode = Node::ExprNode;
/**
* The value of a parameter at function entry, viewed as a node in a data
* flow graph.
@@ -56,4 +60,31 @@ module DataFlow {
predicate localFlow(Node::Node source, Node::Node sink) { localFlowStep*(source, sink) }
import DataFlowMake<Location, DataFlowImpl::RustDataFlow>
/**
* Holds if the guard `g` validates the expression `e` upon evaluating to `v`.
*
* The expression `e` is expected to be a syntactic part of the guard `g`.
* For example, the guard `g` might be a call `isSafe(x)` and the expression `e`
* the argument `x`.
*/
signature predicate guardChecksSig(CfgNodes::AstCfgNode g, Cfg::CfgNode e, boolean branch);
/**
* Provides a set of barrier nodes for a guard that validates an expression.
*
* This is expected to be used in `isBarrier`/`isSanitizer` definitions
* in data flow and taint tracking.
*/
module BarrierGuard<guardChecksSig/3 guardChecks> {
private import internal.DataFlowImpl::SsaFlow as SsaFlow
private import internal.SsaImpl as SsaImpl
/** Gets a node that is safely guarded by the given guard check. */
pragma[nomagic]
Node getABarrierNode() {
SsaFlow::asNode(result) =
SsaImpl::DataFlowIntegration::BarrierGuard<guardChecks/3>::getABarrierNode()
}
}
}

View File

@@ -214,6 +214,41 @@ final class SingletonContentSet extends ContentSet, TSingletonContentSet {
override Content getAReadContent() { result = c }
}
/**
* 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.
*/
final class OptionalStep extends ContentSet, TOptionalStep {
override string toString() {
exists(string name |
this = TOptionalStep(name) and
result = "OptionalStep[" + name + "]"
)
}
override Content getAStoreContent() { none() }
override Content getAReadContent() { none() }
}
/**
* 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.
*/
final class OptionalBarrier extends ContentSet, TOptionalBarrier {
override string toString() {
exists(string name |
this = TOptionalBarrier(name) and
result = "OptionalBarrier[" + name + "]"
)
}
override Content getAStoreContent() { none() }
override Content getAReadContent() { none() }
}
private import codeql.rust.internal.CachedStages
cached

View File

@@ -624,6 +624,12 @@ module RustDataFlow implements InputSig<Location> {
model = ""
or
LocalFlow::flowSummaryLocalStep(nodeFrom, nodeTo, model)
or
// Add flow through optional barriers. This step is then blocked by the barrier for queries that choose to use the barrier.
FlowSummaryImpl::Private::Steps::summaryReadStep(nodeFrom
.(Node::FlowSummaryNode)
.getSummaryNode(), TOptionalBarrier(_), nodeTo.(Node::FlowSummaryNode).getSummaryNode()) and
model = ""
}
/**
@@ -753,7 +759,17 @@ module RustDataFlow implements InputSig<Location> {
)
or
FlowSummaryImpl::Private::Steps::summaryReadStep(node1.(FlowSummaryNode).getSummaryNode(), cs,
node2.(FlowSummaryNode).getSummaryNode())
node2.(FlowSummaryNode).getSummaryNode()) and
not isSpecialContentSet(cs)
}
/**
* Holds if `cs` is used to encode a special operation as a content component, but should not
* be treated as an ordinary content component.
*/
private predicate isSpecialContentSet(ContentSet cs) {
cs instanceof TOptionalStep or
cs instanceof TOptionalBarrier
}
pragma[nomagic]
@@ -850,7 +866,8 @@ module RustDataFlow implements InputSig<Location> {
storeContentStep(node1, cs.(SingletonContentSet).getContent(), node2)
or
FlowSummaryImpl::Private::Steps::summaryStoreStep(node1.(FlowSummaryNode).getSummaryNode(), cs,
node2.(FlowSummaryNode).getSummaryNode())
node2.(FlowSummaryNode).getSummaryNode()) and
not isSpecialContentSet(cs)
}
/**
@@ -1136,7 +1153,14 @@ private module Cached {
newtype TReturnKind = TNormalReturnKind()
cached
newtype TContentSet = TSingletonContentSet(Content c)
newtype TContentSet =
TSingletonContentSet(Content c) or
TOptionalStep(string name) {
name = any(FlowSummaryImpl::Private::AccessPathToken tok).getAnArgument("OptionalStep")
} or
TOptionalBarrier(string name) {
name = any(FlowSummaryImpl::Private::AccessPathToken tok).getAnArgument("OptionalBarrier")
}
/** Holds if `n` is a flow source of kind `kind`. */
cached
@@ -1145,6 +1169,27 @@ private module Cached {
/** Holds if `n` is a flow sink of kind `kind`. */
cached
predicate sinkNode(Node n, string kind) { n.(FlowSummaryNode).isSink(kind, _) }
/**
* 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.
*/
cached
predicate optionalStep(Node node1, string name, Node node2) {
FlowSummaryImpl::Private::Steps::summaryReadStep(node1.(FlowSummaryNode).getSummaryNode(),
TOptionalStep(name), node2.(FlowSummaryNode).getSummaryNode())
}
/**
* 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.
*/
cached
predicate optionalBarrier(Node node, string name) {
FlowSummaryImpl::Private::Steps::summaryReadStep(_, TOptionalBarrier(name),
node.(FlowSummaryNode).getSummaryNode())
}
}
import Cached

View File

@@ -107,6 +107,10 @@ module Input implements InputSig<Location, RustDataFlow> {
c = TFutureContent() and
arg = ""
)
or
cs = TOptionalStep(arg) and result = "OptionalStep"
or
cs = TOptionalBarrier(arg) and result = "OptionalBarrier"
}
string encodeReturn(ReturnKind rk, string arg) { none() }

View File

@@ -72,7 +72,9 @@ module RustTaintTracking implements InputSig<Location, RustDataFlow> {
exists(Content c | c = cs.(SingletonContentSet).getContent() |
c instanceof ElementContent or
c instanceof ReferenceContent
)
) and
// Optional steps are added through isAdditionalFlowStep but we don't want the implicit reads
not optionalStep(node, _, _)
}
/**

View File

@@ -0,0 +1,20 @@
/**
* Provides modeling for the `Poem` library.
*/
private import rust
private import codeql.rust.Concepts
private import codeql.rust.dataflow.DataFlow
/**
* Parameters of a handler function
*/
private class PoemHandlerParam extends RemoteSource::Range {
PoemHandlerParam() {
exists(TupleStructPat param |
param.getResolvedPath() = ["crate::web::query::Query", "crate::web::path::Path"]
|
this.asPat().getPat() = param.getAField()
)
}
}

View File

@@ -0,0 +1,23 @@
/**
* Provides classes modeling security-relevant aspects of the standard libraries.
*/
private import rust
private import codeql.rust.Concepts
private import codeql.rust.controlflow.ControlFlowGraph as Cfg
private import codeql.rust.controlflow.CfgNodes as CfgNodes
private import codeql.rust.dataflow.DataFlow
/**
* A call to the `starts_with` method on a `Path`.
*/
private class StartswithCall extends Path::SafeAccessCheck::Range, CfgNodes::MethodCallExprCfgNode {
StartswithCall() {
this.getAstNode().(Resolvable).getResolvedPath() = "<crate::path::Path>::starts_with"
}
override predicate checks(Cfg::CfgNode e, boolean branch) {
e = this.getReceiver() and
branch = true
}
}

View File

@@ -0,0 +1,46 @@
extensions:
- addsTo:
pack: codeql/rust-all
extensible: sourceModel
data: []
- addsTo:
pack: codeql/rust-all
extensible: sinkModel
data:
- ["lang:std", "crate::fs::copy", "Argument[0]", "path-injection", "manual"]
- ["lang:std", "crate::fs::copy", "Argument[1]", "path-injection", "manual"]
- ["lang:std", "crate::fs::create_dir", "Argument[0]", "path-injection", "manual"]
- ["lang:std", "crate::fs::create_dir_all", "Argument[0]", "path-injection", "manual"]
- ["lang:std", "crate::fs::hard_link", "Argument[0]", "path-injection", "manual"]
- ["lang:std", "crate::fs::hard_link", "Argument[1]", "path-injection", "manual"]
- ["lang:std", "crate::fs::metadata", "Argument[0]", "path-injection", "manual"]
- ["lang:std", "crate::fs::read", "Argument[0]", "path-injection", "manual"]
- ["lang:std", "crate::fs::read_dir", "Argument[0]", "path-injection", "manual"]
- ["lang:std", "crate::fs::read_link", "Argument[0]", "path-injection", "manual"]
- ["lang:std", "crate::fs::read_to_string", "Argument[0]", "path-injection", "manual"]
- ["lang:std", "crate::fs::remove_dir", "Argument[0]", "path-injection", "manual"]
- ["lang:std", "crate::fs::remove_dir_all", "Argument[0]", "path-injection", "manual"]
- ["lang:std", "crate::fs::remove_file", "Argument[0]", "path-injection", "manual"]
- ["lang:std", "crate::fs::rename", "Argument[0]", "path-injection", "manual"]
- ["lang:std", "crate::fs::rename", "Argument[1]", "path-injection", "manual"]
- ["lang:std", "crate::fs::set_permissions", "Argument[0]", "path-injection", "manual"]
- ["lang:std", "crate::fs::soft_link", "Argument[0]", "path-injection", "manual"]
- ["lang:std", "crate::fs::soft_link", "Argument[1]", "path-injection", "manual"]
- ["lang:std", "crate::fs::symlink_metadata", "Argument[0]", "path-injection", "manual"]
- ["lang:std", "crate::fs::write", "Argument[0]", "path-injection", "manual"]
- ["lang:std", "<crate::fs::DirBuilder>::create", "Argument[0]", "path-injection", "manual"]
- ["lang:std", "<crate::fs::File>::create", "Argument[0]", "path-injection", "manual"]
- ["lang:std", "<crate::fs::File>::create_buffered", "Argument[0]", "path-injection", "manual"]
- ["lang:std", "<crate::fs::File>::create_new", "Argument[0]", "path-injection", "manual"]
- ["lang:std", "<crate::fs::File>::open", "Argument[0]", "path-injection", "manual"]
- ["lang:std", "<crate::fs::File>::open_buffered", "Argument[0]", "path-injection", "manual"]
- addsTo:
pack: codeql/rust-all
extensible: summaryModel
data:
- ["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].OptionalStep[normalize-path]", "ReturnValue.Field[crate::result::Result::Ok(0)]", "taint", "manual"]
- ["lang:std", "<crate::path::Path>::canonicalize", "Argument[self].OptionalBarrier[normalize-path]", "ReturnValue.Field[crate::result::Result::Ok(0)]", "taint", "manual"]

View File

@@ -0,0 +1,79 @@
/** Provides classes and predicates to reason about path injection vulnerabilities. */
import rust
private import codeql.rust.controlflow.BasicBlocks
private import codeql.rust.controlflow.ControlFlowGraph
private import codeql.rust.dataflow.DataFlow
private import codeql.rust.dataflow.TaintTracking
private import codeql.rust.Concepts
private import codeql.rust.dataflow.internal.DataFlowImpl
private import codeql.rust.controlflow.ControlFlowGraph as Cfg
private import codeql.rust.controlflow.CfgNodes as CfgNodes
/**
* Provides default sources, sinks and barriers for detecting path injection
* vulnerabilities, as well as extension points for adding your own.
*/
module TaintedPath {
/**
* A data flow source for path injection vulnerabilities.
*/
abstract class Source extends DataFlow::Node { }
/**
* A data flow sink for path injection vulnerabilities.
*/
abstract class Sink extends DataFlow::Node { }
/**
* A barrier for path injection vulnerabilities.
*/
abstract class Barrier extends DataFlow::Node { }
/**
* A sanitizer guard for path-traversal vulnerabilities.
*/
class SanitizerGuard extends DataFlow::Node {
SanitizerGuard() { this = DataFlow::BarrierGuard<sanitizerGuard/3>::getABarrierNode() }
}
/**
* An active threat-model source, considered as a flow source.
*/
private class ActiveThreatModelSourceAsSource extends Source, ActiveThreatModelSource { }
/** A sink for path-injection from model data. */
private class ModelsAsDataSinks extends Sink {
ModelsAsDataSinks() { sinkNode(this, "path-injection") }
}
}
private predicate sanitizerGuard(CfgNodes::AstCfgNode g, Cfg::CfgNode node, boolean branch) {
g.(SanitizerGuard::Range).checks(node, branch)
}
/** Provides a class for modeling new path safety checks. */
module SanitizerGuard {
/** A data-flow node that checks that a path is safe to access. */
abstract class Range extends CfgNodes::AstCfgNode {
/** Holds if this guard validates `node` upon evaluating to `branch`. */
abstract predicate checks(Cfg::CfgNode node, boolean branch);
}
}
/**
* A check of the form `!strings.Contains(nd, "..")`, considered as a sanitizer guard for
* path traversal.
*/
private class DotDotCheck extends SanitizerGuard::Range, CfgNodes::MethodCallExprCfgNode {
DotDotCheck() {
this.getAstNode().(Resolvable).getResolvedPath() = "<str>::contains" and
this.getArgument(0).getAstNode().(LiteralExpr).getTextValue() =
["\"..\"", "\"../\"", "\"..\\\""]
}
override predicate checks(Cfg::CfgNode e, boolean branch) {
e = this.getReceiver() and
branch = false
}
}

View File

@@ -0,0 +1,67 @@
<!DOCTYPE qhelp PUBLIC
"-//Semmle//qhelp//EN"
"qhelp.dtd">
<qhelp>
<overview>
<p>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.</p>
<p>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.</p>
</overview>
<recommendation>
<p>Validate user input before using it to construct a file path.</p>
<p>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.
</p>
<p>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.
</p>
<p>
Note that removing "../" sequences is <i>not</i> 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.
</p>
<p>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.</p>
</recommendation>
<example>
<p>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".</p>
<sample src="examples/TaintedPath.rs" />
<p>
If the input should only be a file name, you can check that it doesn't contain any path separators or ".." sequences.
</p>
<sample src="examples/TaintedPathGoodNormalize.rs" />
<p>
If the input should be within a specific directory, you can check that the resolved path
is still contained within that directory.
</p>
<sample src="examples/TaintedPathGoodFolder.rs" />
</example>
<references>
<li>
OWASP:
<a href="https://owasp.org/www-community/attacks/Path_Traversal">Path Traversal</a>.
</li>
</references>
</qhelp>

View File

@@ -0,0 +1,90 @@
/**
* @name Uncontrolled data used in path expression
* @description Accessing paths influenced by users can allow an attacker to access unexpected resources.
* @kind path-problem
* @problem.severity error
* @security-severity 7.5
* @precision high
* @id rust/path-injection
* @tags security
* external/cwe/cwe-022
* external/cwe/cwe-023
* external/cwe/cwe-036
* external/cwe/cwe-073
* external/cwe/cwe-099
*/
import rust
import codeql.rust.dataflow.DataFlow
import codeql.rust.dataflow.internal.DataFlowImpl as DataflowImpl
import codeql.rust.dataflow.TaintTracking
import codeql.rust.security.TaintedPathExtensions
import TaintedPathFlow::PathGraph
private import codeql.rust.Concepts
newtype NormalizationState =
/** A state signifying that the file path has not been normalized. */
NotNormalized() or
/** A state signifying that the file path has been normalized, but not checked. */
NormalizedUnchecked()
/**
* This configuration uses two flow states, `NotNormalized` and `NormalizedUnchecked`,
* to track the requirement that a file path must be first normalized and then checked
* before it is safe to use.
*
* At sources, paths are assumed not normalized. At normalization points, they change
* state to `NormalizedUnchecked` after which they can be made safe by an appropriate
* check of the prefix.
*
* Such checks are ineffective in the `NotNormalized` state.
*/
module TaintedPathConfig implements DataFlow::StateConfigSig {
class FlowState = NormalizationState;
predicate isSource(DataFlow::Node source, FlowState state) {
source instanceof TaintedPath::Source and state instanceof NotNormalized
}
predicate isSink(DataFlow::Node sink, FlowState state) {
sink instanceof TaintedPath::Sink and
(
state instanceof NotNormalized or
state instanceof NormalizedUnchecked
)
}
predicate isBarrier(DataFlow::Node node) {
node instanceof TaintedPath::Barrier or node instanceof TaintedPath::SanitizerGuard
}
predicate isBarrier(DataFlow::Node node, FlowState state) {
// Block `NotNormalized` paths here, since they change state to `NormalizedUnchecked`
(
node instanceof Path::PathNormalization or
DataflowImpl::optionalBarrier(node, "normalize-path")
) and
state instanceof NotNormalized
or
node instanceof Path::SafeAccessCheck and
state instanceof NormalizedUnchecked
}
predicate isAdditionalFlowStep(
DataFlow::Node nodeFrom, FlowState stateFrom, DataFlow::Node nodeTo, FlowState stateTo
) {
(
nodeFrom = nodeTo.(Path::PathNormalization).getPathArg() or
DataflowImpl::optionalStep(nodeFrom, "normalize-path", nodeTo)
) and
stateFrom instanceof NotNormalized and
stateTo instanceof NormalizedUnchecked
}
}
module TaintedPathFlow = TaintTracking::GlobalWithState<TaintedPathConfig>;
from TaintedPathFlow::PathNode source, TaintedPathFlow::PathNode sink
where TaintedPathFlow::flowPath(source, sink)
select sink.getNode(), source, sink, "This path depends on a $@.", source.getNode(),
"user-provided value"

View File

@@ -0,0 +1,9 @@
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)
}

View File

@@ -0,0 +1,14 @@
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)
}

View File

@@ -0,0 +1,12 @@
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)
}

View File

@@ -1,4 +1,5 @@
localStep
| file://:0:0:0:0 | [summary param] self in lang:std::_::<crate::path::Path>::canonicalize | file://:0:0:0:0 | [summary] read: Argument[self].OptionalBarrier[normalize-path] in lang:std::_::<crate::path::Path>::canonicalize |
| main.rs:3:11:3:11 | [SSA] i | main.rs:4:12:4:12 | i |
| main.rs:3:11:3:11 | i | main.rs:3:11:3:11 | [SSA] i |
| main.rs:3:11:3:11 | i | main.rs:3:11:3:11 | i |
@@ -2189,6 +2190,7 @@ storeStep
| file://:0:0:0:0 | [summary] to write: ReturnValue.Field[crate::result::Result::Ok(0)] in lang:core::_::<crate::result::Result>::or_else | Ok | file://:0:0:0:0 | [summary] to write: ReturnValue in lang:core::_::<crate::result::Result>::or_else |
| file://:0:0:0:0 | [summary] to write: ReturnValue.Field[crate::result::Result::Ok(0)] in lang:core::_::<str>::parse | Ok | file://:0:0:0:0 | [summary] to write: ReturnValue in lang:core::_::<str>::parse |
| file://:0:0:0:0 | [summary] to write: ReturnValue.Field[crate::result::Result::Ok(0)] in lang:std::_::<&[u8] as crate::io::BufRead>::fill_buf | Ok | file://:0:0:0:0 | [summary] to write: ReturnValue in lang:std::_::<&[u8] as crate::io::BufRead>::fill_buf |
| file://:0:0:0:0 | [summary] to write: ReturnValue.Field[crate::result::Result::Ok(0)] in lang:std::_::<crate::path::Path>::canonicalize | Ok | file://:0:0:0:0 | [summary] to write: ReturnValue in lang:std::_::<crate::path::Path>::canonicalize |
| file://:0:0:0:0 | [summary] to write: ReturnValue.Field[crate::result::Result::Ok(0)] in lang:std::_::<crate::sync::poison::condvar::Condvar>::wait | Ok | file://:0:0:0:0 | [summary] to write: ReturnValue in lang:std::_::<crate::sync::poison::condvar::Condvar>::wait |
| file://:0:0:0:0 | [summary] to write: ReturnValue.Field[crate::result::Result::Ok(0)] in lang:std::_::<crate::sync::poison::condvar::Condvar>::wait_timeout | Ok | file://:0:0:0:0 | [summary] to write: ReturnValue in lang:std::_::<crate::sync::poison::condvar::Condvar>::wait_timeout |
| file://:0:0:0:0 | [summary] to write: ReturnValue.Field[crate::result::Result::Ok(0)] in lang:std::_::<crate::sync::poison::condvar::Condvar>::wait_timeout_ms | Ok | file://:0:0:0:0 | [summary] to write: ReturnValue in lang:std::_::<crate::sync::poison::condvar::Condvar>::wait_timeout_ms |

View File

@@ -15,7 +15,7 @@
| Macro calls - resolved | 8 |
| Macro calls - total | 9 |
| Macro calls - unresolved | 1 |
| Taint edges - number of edges | 1671 |
| Taint edges - number of edges | 1674 |
| Taint reach - nodes tainted | 0 |
| Taint reach - per million nodes | 0 |
| Taint sinks - cryptographic operations | 0 |

View File

@@ -3,14 +3,14 @@
edges
| main.rs:4:9:4:16 | username | main.rs:5:25:5:44 | MacroExpr | provenance | |
| main.rs:4:20:4:32 | ...::var | main.rs:4:20:4:40 | ...::var(...) [Ok] | provenance | Src:MaD:62 |
| main.rs:4:20:4:40 | ...::var(...) [Ok] | main.rs:4:20:4:66 | ... .unwrap_or(...) | provenance | MaD:1593 |
| main.rs:4:20:4:40 | ...::var(...) [Ok] | main.rs:4:20:4:66 | ... .unwrap_or(...) | provenance | MaD:1625 |
| main.rs:4:20:4:66 | ... .unwrap_or(...) | main.rs:4:9:4:16 | username | provenance | |
| main.rs:5:9:5:13 | regex | main.rs:6:26:6:30 | regex | provenance | |
| main.rs:5:17:5:45 | res | main.rs:5:25:5:44 | { ... } | provenance | |
| main.rs:5:25:5:44 | ...::format(...) | main.rs:5:17:5:45 | res | provenance | |
| main.rs:5:25:5:44 | ...::must_use(...) | main.rs:5:9:5:13 | regex | provenance | |
| main.rs:5:25:5:44 | MacroExpr | main.rs:5:25:5:44 | ...::format(...) | provenance | MaD:66 |
| main.rs:5:25:5:44 | { ... } | main.rs:5:25:5:44 | ...::must_use(...) | provenance | MaD:3016 |
| main.rs:5:25:5:44 | MacroExpr | main.rs:5:25:5:44 | ...::format(...) | provenance | MaD:98 |
| main.rs:5:25:5:44 | { ... } | main.rs:5:25:5:44 | ...::must_use(...) | provenance | MaD:3048 |
| main.rs:6:26:6:30 | regex | main.rs:6:25:6:30 | &regex | provenance | |
nodes
| main.rs:4:9:4:16 | username | semmle.label | username |

View File

@@ -0,0 +1,55 @@
#select
| src/main.rs:10:5:10:22 | ...::read_to_string | src/main.rs:6:11:6:19 | file_name | src/main.rs:10:5:10:22 | ...::read_to_string | This path depends on a $@. | src/main.rs:6:11:6:19 | file_name | user-provided value |
| src/main.rs:45:5:45:22 | ...::read_to_string | src/main.rs:37:11:37:19 | file_path | src/main.rs:45:5:45:22 | ...::read_to_string | This path depends on a $@. | src/main.rs:37:11:37:19 | file_path | user-provided value |
| src/main.rs:59:5:59:22 | ...::read_to_string | src/main.rs:50:11:50:19 | file_path | src/main.rs:59:5:59:22 | ...::read_to_string | This path depends on a $@. | src/main.rs:50:11:50:19 | file_path | user-provided value |
edges
| src/main.rs:6:11:6:19 | file_name | src/main.rs:8:35:8:43 | file_name | provenance | |
| src/main.rs:8:9:8:17 | file_path | src/main.rs:10:24:10:32 | file_path | provenance | |
| src/main.rs:8:21:8:44 | ...::from(...) | src/main.rs:8:9:8:17 | file_path | provenance | |
| src/main.rs:8:35:8:43 | file_name | src/main.rs:8:21:8:44 | ...::from(...) | provenance | MaD:4 |
| src/main.rs:10:24:10:32 | file_path | src/main.rs:10:5:10:22 | ...::read_to_string | provenance | MaD:1 Sink:MaD:1 |
| src/main.rs:37:11:37:19 | file_path | src/main.rs:40:52:40:60 | file_path | provenance | |
| src/main.rs:40:9:40:17 | file_path | src/main.rs:45:24:45:32 | file_path | provenance | |
| src/main.rs:40:21:40:62 | public_path.join(...) | src/main.rs:40:9:40:17 | file_path | provenance | |
| src/main.rs:40:38:40:61 | ...::from(...) | src/main.rs:40:21:40:62 | public_path.join(...) | provenance | MaD:3 |
| src/main.rs:40:52:40:60 | file_path | src/main.rs:40:38:40:61 | ...::from(...) | provenance | MaD:4 |
| src/main.rs:45:24:45:32 | file_path | src/main.rs:45:5:45:22 | ...::read_to_string | provenance | MaD:1 Sink:MaD:1 |
| src/main.rs:50:11:50:19 | file_path | src/main.rs:53:52:53:60 | file_path | provenance | |
| src/main.rs:53:9:53:17 | file_path | src/main.rs:54:21:54:44 | file_path.canonicalize(...) [Ok] | provenance | Config |
| src/main.rs:53:21:53:62 | public_path.join(...) | src/main.rs:53:9:53:17 | file_path | provenance | |
| src/main.rs:53:38:53:61 | ...::from(...) | src/main.rs:53:21:53:62 | public_path.join(...) | provenance | MaD:3 |
| src/main.rs:53:52:53:60 | file_path | src/main.rs:53:38:53:61 | ...::from(...) | provenance | MaD:4 |
| src/main.rs:54:9:54:17 | file_path | src/main.rs:59:24:59:32 | file_path | provenance | |
| src/main.rs:54:21:54:44 | file_path.canonicalize(...) [Ok] | src/main.rs:54:21:54:53 | ... .unwrap(...) | provenance | MaD:2 |
| src/main.rs:54:21:54:53 | ... .unwrap(...) | src/main.rs:54:9:54:17 | file_path | provenance | |
| src/main.rs:59:24:59:32 | file_path | src/main.rs:59:5:59:22 | ...::read_to_string | provenance | MaD:1 Sink:MaD:1 |
models
| 1 | Sink: lang:std; crate::fs::read_to_string; path-injection; Argument[0] |
| 2 | Summary: lang:core; <crate::result::Result>::unwrap; Argument[self].Field[crate::result::Result::Ok(0)]; ReturnValue; value |
| 3 | Summary: lang:std; <crate::path::Path>::join; Argument[0]; ReturnValue; taint |
| 4 | Summary: lang:std; <crate::path::PathBuf as crate::convert::From>::from; Argument[0]; ReturnValue; taint |
nodes
| src/main.rs:6:11:6:19 | file_name | semmle.label | file_name |
| src/main.rs:8:9:8:17 | file_path | semmle.label | file_path |
| src/main.rs:8:21:8:44 | ...::from(...) | semmle.label | ...::from(...) |
| src/main.rs:8:35:8:43 | file_name | semmle.label | file_name |
| src/main.rs:10:5:10:22 | ...::read_to_string | semmle.label | ...::read_to_string |
| src/main.rs:10:24:10:32 | file_path | semmle.label | file_path |
| src/main.rs:37:11:37:19 | file_path | semmle.label | file_path |
| src/main.rs:40:9:40:17 | file_path | semmle.label | file_path |
| src/main.rs:40:21:40:62 | public_path.join(...) | semmle.label | public_path.join(...) |
| src/main.rs:40:38:40:61 | ...::from(...) | semmle.label | ...::from(...) |
| src/main.rs:40:52:40:60 | file_path | semmle.label | file_path |
| src/main.rs:45:5:45:22 | ...::read_to_string | semmle.label | ...::read_to_string |
| src/main.rs:45:24:45:32 | file_path | semmle.label | file_path |
| src/main.rs:50:11:50:19 | file_path | semmle.label | file_path |
| src/main.rs:53:9:53:17 | file_path | semmle.label | file_path |
| src/main.rs:53:21:53:62 | public_path.join(...) | semmle.label | public_path.join(...) |
| src/main.rs:53:38:53:61 | ...::from(...) | semmle.label | ...::from(...) |
| src/main.rs:53:52:53:60 | file_path | semmle.label | file_path |
| src/main.rs:54:9:54:17 | file_path | semmle.label | file_path |
| src/main.rs:54:21:54:44 | file_path.canonicalize(...) [Ok] | semmle.label | file_path.canonicalize(...) [Ok] |
| src/main.rs:54:21:54:53 | ... .unwrap(...) | semmle.label | ... .unwrap(...) |
| src/main.rs:59:5:59:22 | ...::read_to_string | semmle.label | ...::read_to_string |
| src/main.rs:59:24:59:32 | file_path | semmle.label | file_path |
subpaths

View File

@@ -0,0 +1,4 @@
query: queries/security/CWE-022/TaintedPath.ql
postprocess:
- utils/test/PrettyPrintModels.ql
- utils/test/InlineExpectationsTestQuery.ql

View File

@@ -0,0 +1,19 @@
import rust
import codeql.rust.security.TaintedPathExtensions
import utils.test.InlineExpectationsTest
module TaintedPathSinksTest implements TestSig {
string getARelevantTag() { result = "path-injection-sink" }
predicate hasActualResult(Location location, string element, string tag, string value) {
exists(TaintedPath::Sink sink |
location = sink.getLocation() and
location.getFile().getBaseName() != "" and
element = sink.toString() and
tag = "path-injection-sink" and
value = ""
)
}
}
import MakeTest<TaintedPathSinksTest>

View File

@@ -0,0 +1,2 @@
qltest_dependencies:
- poem = { version = "3.1.7" }

View File

@@ -0,0 +1,8 @@
# This file specifies the Rust version used to develop and test the
# extractors written in rust. It is set to the lowest version of Rust
# we want to support.
[toolchain]
channel = "nightly"
profile = "minimal"
components = [ ]

View File

@@ -0,0 +1,88 @@
#![feature(file_buffered)]
use poem::{error::InternalServerError, handler, http::StatusCode, web::Query, Error, Result};
use std::{fs, path::Path, path::PathBuf};
//#[handler]
fn tainted_path_handler_bad(
Query(file_name): Query<String>, // $ Source=remote1
) -> 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) // $ path-injection-sink Alert[rust/path-injection]=remote1
}
//#[handler]
fn tainted_path_handler_good(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) // $ path-injection-sink
}
//#[handler]
fn tainted_path_handler_folder_good(Query(file_path): Query<String>) -> Result<String> {
let public_path = PathBuf::from("/var/www/public_html");
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) // $ path-injection-sink
}
//#[handler]
fn tainted_path_handler_folder_almost_good1(
Query(file_path): Query<String>, // $ Source=remote4
) -> Result<String> {
let public_path = PathBuf::from("/var/www/public_html");
let file_path = public_path.join(PathBuf::from(file_path));
// BAD: the path could still contain `..` and escape 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) // $ path-injection-sink Alert[rust/path-injection]=remote4
}
//#[handler]
fn tainted_path_handler_folder_almost_good2(
Query(file_path): Query<String>, // $ Source=remote5
) -> Result<String> {
let public_path = PathBuf::from("/var/www/public_html");
let file_path = public_path.join(PathBuf::from(file_path));
let file_path = file_path.canonicalize().unwrap();
// BAD: the check to ensure that the path stays within the public folder is wrong
if file_path.starts_with(public_path) {
return Err(Error::from_status(StatusCode::BAD_REQUEST));
}
fs::read_to_string(file_path).map_err(InternalServerError) // $ path-injection-sink Alert[rust/path-injection]=remote5
}
fn sinks(path1: &Path, path2: &Path) {
let _ = std::fs::copy(path1, path2); // $ path-injection-sink
let _ = std::fs::create_dir(path1); // $ path-injection-sink
let _ = std::fs::create_dir_all(path1); // $ path-injection-sink
let _ = std::fs::hard_link(path1, path2); // $ path-injection-sink
let _ = std::fs::metadata(path1); // $ path-injection-sink
let _ = std::fs::read(path1); // $ path-injection-sink
let _ = std::fs::read_dir(path1); // $ path-injection-sink
let _ = std::fs::read_link(path1); // $ path-injection-sink
let _ = std::fs::read_to_string(path1); // $ path-injection-sink
let _ = std::fs::remove_dir(path1); // $ path-injection-sink
let _ = std::fs::remove_dir_all(path1); // $ path-injection-sink
let _ = std::fs::remove_file(path1); // $ path-injection-sink
let _ = std::fs::rename(path1, path2); // $ path-injection-sink
let _ = std::fs::set_permissions(path1, std::os::unix::fs::PermissionsExt::from_mode(7)); // $ path-injection-sink
let _ = std::fs::soft_link(path1, path2); // $ path-injection-sink
let _ = std::fs::symlink_metadata(path1); // $ path-injection-sink
let _ = std::fs::write(path1, "contents"); // $ path-injection-sink
let _ = std::fs::DirBuilder::new().create(path1); // $ path-injection-sink
let _ = std::fs::File::create(path1); // $ path-injection-sink
let _ = std::fs::File::create_buffered(path1); // $ path-injection-sink
let _ = std::fs::File::create_new(path1); // $ path-injection-sink
let _ = std::fs::File::open(path1); // $ path-injection-sink
let _ = std::fs::File::open_buffered(path1); // $ path-injection-sink
}
fn main() {}