mirror of
https://github.com/github/codeql.git
synced 2025-12-16 16:53:25 +01:00
Merge pull request #18960 from github/aibaars/rust-tainted-path
Rust: TaintedPath query
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() }
|
||||
|
||||
@@ -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, _, _)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
20
rust/ql/lib/codeql/rust/frameworks/Poem.qll
Normal file
20
rust/ql/lib/codeql/rust/frameworks/Poem.qll
Normal 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()
|
||||
)
|
||||
}
|
||||
}
|
||||
23
rust/ql/lib/codeql/rust/frameworks/stdlib/Stdlib.qll
Normal file
23
rust/ql/lib/codeql/rust/frameworks/stdlib/Stdlib.qll
Normal 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
|
||||
}
|
||||
}
|
||||
46
rust/ql/lib/codeql/rust/frameworks/stdlib/fs.model.yml
Normal file
46
rust/ql/lib/codeql/rust/frameworks/stdlib/fs.model.yml
Normal 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"]
|
||||
79
rust/ql/lib/codeql/rust/security/TaintedPathExtensions.qll
Normal file
79
rust/ql/lib/codeql/rust/security/TaintedPathExtensions.qll
Normal 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
|
||||
}
|
||||
}
|
||||
67
rust/ql/src/queries/security/CWE-022/TaintedPath.qhelp
Normal file
67
rust/ql/src/queries/security/CWE-022/TaintedPath.qhelp
Normal 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>
|
||||
90
rust/ql/src/queries/security/CWE-022/TaintedPath.ql
Normal file
90
rust/ql/src/queries/security/CWE-022/TaintedPath.ql
Normal 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"
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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 |
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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 | ®ex | provenance | |
|
||||
nodes
|
||||
| main.rs:4:9:4:16 | username | semmle.label | username |
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,4 @@
|
||||
query: queries/security/CWE-022/TaintedPath.ql
|
||||
postprocess:
|
||||
- utils/test/PrettyPrintModels.ql
|
||||
- utils/test/InlineExpectationsTestQuery.ql
|
||||
@@ -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>
|
||||
2
rust/ql/test/query-tests/security/CWE-022/options.yml
Normal file
2
rust/ql/test/query-tests/security/CWE-022/options.yml
Normal file
@@ -0,0 +1,2 @@
|
||||
qltest_dependencies:
|
||||
- poem = { version = "3.1.7" }
|
||||
@@ -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 = [ ]
|
||||
88
rust/ql/test/query-tests/security/CWE-022/src/main.rs
Normal file
88
rust/ql/test/query-tests/security/CWE-022/src/main.rs
Normal 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() {}
|
||||
Reference in New Issue
Block a user