diff --git a/python/ql/lib/semmle/python/Concepts.qll b/python/ql/lib/semmle/python/Concepts.qll index 94d660d75108..27f622c7c868 100644 --- a/python/ql/lib/semmle/python/Concepts.qll +++ b/python/ql/lib/semmle/python/Concepts.qll @@ -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::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); diff --git a/rust/ql/integration-tests/hello-project/summary.expected b/rust/ql/integration-tests/hello-project/summary.expected index 3599902243cd..1dd49972c22b 100644 --- a/rust/ql/integration-tests/hello-project/summary.expected +++ b/rust/ql/integration-tests/hello-project/summary.expected @@ -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 | diff --git a/rust/ql/integration-tests/hello-workspace/summary.cargo.expected b/rust/ql/integration-tests/hello-workspace/summary.cargo.expected index 3fbea6c46417..53f9c345b96d 100644 --- a/rust/ql/integration-tests/hello-workspace/summary.cargo.expected +++ b/rust/ql/integration-tests/hello-workspace/summary.cargo.expected @@ -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 | diff --git a/rust/ql/integration-tests/hello-workspace/summary.rust-project.expected b/rust/ql/integration-tests/hello-workspace/summary.rust-project.expected index 3fbea6c46417..53f9c345b96d 100644 --- a/rust/ql/integration-tests/hello-workspace/summary.rust-project.expected +++ b/rust/ql/integration-tests/hello-workspace/summary.rust-project.expected @@ -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 | diff --git a/rust/ql/lib/codeql/rust/Concepts.qll b/rust/ql/lib/codeql/rust/Concepts.qll index 4f25840165e7..723cde6913a8 100644 --- a/rust/ql/lib/codeql/rust/Concepts.qll +++ b/rust/ql/lib/codeql/rust/Concepts.qll @@ -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::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); + } + } +} diff --git a/rust/ql/lib/codeql/rust/Frameworks.qll b/rust/ql/lib/codeql/rust/Frameworks.qll index daa96538e21f..0e91ed427ba4 100644 --- a/rust/ql/lib/codeql/rust/Frameworks.qll +++ b/rust/ql/lib/codeql/rust/Frameworks.qll @@ -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 diff --git a/rust/ql/lib/codeql/rust/dataflow/DataFlow.qll b/rust/ql/lib/codeql/rust/dataflow/DataFlow.qll index 3cb4dd9a982f..e6b7cdfeb50e 100644 --- a/rust/ql/lib/codeql/rust/dataflow/DataFlow.qll +++ b/rust/ql/lib/codeql/rust/dataflow/DataFlow.qll @@ -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 + + /** + * 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 { + 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::getABarrierNode() + } + } } diff --git a/rust/ql/lib/codeql/rust/dataflow/internal/Content.qll b/rust/ql/lib/codeql/rust/dataflow/internal/Content.qll index f9ad0a02cb71..d67085c62f83 100644 --- a/rust/ql/lib/codeql/rust/dataflow/internal/Content.qll +++ b/rust/ql/lib/codeql/rust/dataflow/internal/Content.qll @@ -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 diff --git a/rust/ql/lib/codeql/rust/dataflow/internal/DataFlowImpl.qll b/rust/ql/lib/codeql/rust/dataflow/internal/DataFlowImpl.qll index b589fe4ad6f9..0045279c615a 100644 --- a/rust/ql/lib/codeql/rust/dataflow/internal/DataFlowImpl.qll +++ b/rust/ql/lib/codeql/rust/dataflow/internal/DataFlowImpl.qll @@ -581,6 +581,12 @@ module RustDataFlow implements InputSig { 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 = "" } /** @@ -710,7 +716,17 @@ module RustDataFlow implements InputSig { ) 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] @@ -807,7 +823,8 @@ module RustDataFlow implements InputSig { 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) } /** @@ -1093,7 +1110,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 @@ -1102,6 +1126,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 diff --git a/rust/ql/lib/codeql/rust/dataflow/internal/FlowSummaryImpl.qll b/rust/ql/lib/codeql/rust/dataflow/internal/FlowSummaryImpl.qll index 31c5b5b01aaa..97c04517b357 100644 --- a/rust/ql/lib/codeql/rust/dataflow/internal/FlowSummaryImpl.qll +++ b/rust/ql/lib/codeql/rust/dataflow/internal/FlowSummaryImpl.qll @@ -105,6 +105,10 @@ module Input implements InputSig { 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() } diff --git a/rust/ql/lib/codeql/rust/dataflow/internal/TaintTrackingImpl.qll b/rust/ql/lib/codeql/rust/dataflow/internal/TaintTrackingImpl.qll index 7cef3b58f554..8e0bd7947201 100644 --- a/rust/ql/lib/codeql/rust/dataflow/internal/TaintTrackingImpl.qll +++ b/rust/ql/lib/codeql/rust/dataflow/internal/TaintTrackingImpl.qll @@ -69,7 +69,9 @@ module RustTaintTracking implements InputSig { 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, _, _) } /** diff --git a/rust/ql/lib/codeql/rust/frameworks/Poem.qll b/rust/ql/lib/codeql/rust/frameworks/Poem.qll new file mode 100644 index 000000000000..66c01a415a1c --- /dev/null +++ b/rust/ql/lib/codeql/rust/frameworks/Poem.qll @@ -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() + ) + } +} diff --git a/rust/ql/lib/codeql/rust/frameworks/stdlib/Stdlib.qll b/rust/ql/lib/codeql/rust/frameworks/stdlib/Stdlib.qll new file mode 100644 index 000000000000..9fc0e70833bc --- /dev/null +++ b/rust/ql/lib/codeql/rust/frameworks/stdlib/Stdlib.qll @@ -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() = "::starts_with" + } + + override predicate checks(Cfg::CfgNode e, boolean branch) { + e = this.getReceiver() and + branch = true + } +} diff --git a/rust/ql/lib/codeql/rust/frameworks/stdlib/fs.model.yml b/rust/ql/lib/codeql/rust/frameworks/stdlib/fs.model.yml new file mode 100644 index 000000000000..c7fa72793306 --- /dev/null +++ b/rust/ql/lib/codeql/rust/frameworks/stdlib/fs.model.yml @@ -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", "::create", "Argument[0]", "path-injection", "manual"] + - ["lang:std", "::create", "Argument[0]", "path-injection", "manual"] + - ["lang:std", "::create_buffered", "Argument[0]", "path-injection", "manual"] + - ["lang:std", "::create_new", "Argument[0]", "path-injection", "manual"] + - ["lang:std", "::open", "Argument[0]", "path-injection", "manual"] + - ["lang:std", "::open_buffered", "Argument[0]", "path-injection", "manual"] + + - addsTo: + pack: codeql/rust-all + extensible: summaryModel + data: + - ["lang:std", "::from", "Argument[0]", "ReturnValue", "taint", "manual"] + - ["lang:std", "::join", "Argument[self]", "ReturnValue", "taint", "manual"] + - ["lang:std", "::join", "Argument[0]", "ReturnValue", "taint", "manual"] + - ["lang:std", "::canonicalize", "Argument[self].OptionalStep[normalize-path]", "ReturnValue.Field[crate::result::Result::Ok(0)]", "taint", "manual"] + - ["lang:std", "::canonicalize", "Argument[self].OptionalBarrier[normalize-path]", "ReturnValue.Field[crate::result::Result::Ok(0)]", "taint", "manual"] diff --git a/rust/ql/lib/codeql/rust/security/TaintedPathExtensions.qll b/rust/ql/lib/codeql/rust/security/TaintedPathExtensions.qll new file mode 100644 index 000000000000..df8c43212b34 --- /dev/null +++ b/rust/ql/lib/codeql/rust/security/TaintedPathExtensions.qll @@ -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::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() = "::contains" and + this.getArgument(0).getAstNode().(LiteralExpr).getTextValue() = + ["\"..\"", "\"../\"", "\"..\\\""] + } + + override predicate checks(Cfg::CfgNode e, boolean branch) { + e = this.getReceiver() and + branch = false + } +} diff --git a/rust/ql/src/queries/security/CWE-022/TaintedPath.qhelp b/rust/ql/src/queries/security/CWE-022/TaintedPath.qhelp new file mode 100644 index 000000000000..8f74c60cfc29 --- /dev/null +++ b/rust/ql/src/queries/security/CWE-022/TaintedPath.qhelp @@ -0,0 +1,67 @@ + + + +

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.

+ +
+ + +

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.

+ +
+ + +

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

+ + + +

+If the input should only be a file name, you can check that it doesn't contain any path separators or ".." sequences. +

+ + + +

+If the input should be within a specific directory, you can check that the resolved path +is still contained within that directory. +

+ + + +
+ + +
  • +OWASP: +Path Traversal. +
  • + +
    +
    diff --git a/rust/ql/src/queries/security/CWE-022/TaintedPath.ql b/rust/ql/src/queries/security/CWE-022/TaintedPath.ql new file mode 100644 index 000000000000..fcc1c89ef662 --- /dev/null +++ b/rust/ql/src/queries/security/CWE-022/TaintedPath.ql @@ -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; + +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" diff --git a/rust/ql/src/queries/security/CWE-022/examples/TaintedPath.rs b/rust/ql/src/queries/security/CWE-022/examples/TaintedPath.rs new file mode 100644 index 000000000000..fb0fdbbf6f55 --- /dev/null +++ b/rust/ql/src/queries/security/CWE-022/examples/TaintedPath.rs @@ -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) -> Result { + 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) +} diff --git a/rust/ql/src/queries/security/CWE-022/examples/TaintedPathGoodFolder.rs b/rust/ql/src/queries/security/CWE-022/examples/TaintedPathGoodFolder.rs new file mode 100644 index 000000000000..8602ad4d5c39 --- /dev/null +++ b/rust/ql/src/queries/security/CWE-022/examples/TaintedPathGoodFolder.rs @@ -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) -> Result { + 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) +} diff --git a/rust/ql/src/queries/security/CWE-022/examples/TaintedPathGoodNormalize.rs b/rust/ql/src/queries/security/CWE-022/examples/TaintedPathGoodNormalize.rs new file mode 100644 index 000000000000..d36cb0e36585 --- /dev/null +++ b/rust/ql/src/queries/security/CWE-022/examples/TaintedPathGoodNormalize.rs @@ -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) -> Result { + // 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) +} diff --git a/rust/ql/test/library-tests/dataflow/local/DataFlowStep.expected b/rust/ql/test/library-tests/dataflow/local/DataFlowStep.expected index 23270af042a6..f7633aed33e4 100644 --- a/rust/ql/test/library-tests/dataflow/local/DataFlowStep.expected +++ b/rust/ql/test/library-tests/dataflow/local/DataFlowStep.expected @@ -1,4 +1,5 @@ localStep +| file://:0:0:0:0 | [summary param] self in lang:std::_::::canonicalize | file://:0:0:0:0 | [summary] read: Argument[self].OptionalBarrier[normalize-path] in lang:std::_::::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 | @@ -2165,6 +2166,7 @@ storeStep | file://:0:0:0:0 | [summary] to write: ReturnValue.Field[crate::result::Result::Ok(0)] in lang:core::_::::or_else | Ok | file://:0:0:0:0 | [summary] to write: ReturnValue in lang:core::_::::or_else | | file://:0:0:0:0 | [summary] to write: ReturnValue.Field[crate::result::Result::Ok(0)] in lang:core::_::::parse | Ok | file://:0:0:0:0 | [summary] to write: ReturnValue in lang:core::_::::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::_::::canonicalize | Ok | file://:0:0:0:0 | [summary] to write: ReturnValue in lang:std::_::::canonicalize | | file://:0:0:0:0 | [summary] to write: ReturnValue.Field[crate::result::Result::Ok(0)] in lang:std::_::::wait | Ok | file://:0:0:0:0 | [summary] to write: ReturnValue in lang:std::_::::wait | | file://:0:0:0:0 | [summary] to write: ReturnValue.Field[crate::result::Result::Ok(0)] in lang:std::_::::wait_timeout | Ok | file://:0:0:0:0 | [summary] to write: ReturnValue in lang:std::_::::wait_timeout | | file://:0:0:0:0 | [summary] to write: ReturnValue.Field[crate::result::Result::Ok(0)] in lang:std::_::::wait_timeout_ms | Ok | file://:0:0:0:0 | [summary] to write: ReturnValue in lang:std::_::::wait_timeout_ms | diff --git a/rust/ql/test/query-tests/diagnostics/SummaryStats.expected b/rust/ql/test/query-tests/diagnostics/SummaryStats.expected index aaf68558f09c..d34cd849069b 100644 --- a/rust/ql/test/query-tests/diagnostics/SummaryStats.expected +++ b/rust/ql/test/query-tests/diagnostics/SummaryStats.expected @@ -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 | diff --git a/rust/ql/test/query-tests/security/CWE-020/RegexInjection.expected b/rust/ql/test/query-tests/security/CWE-020/RegexInjection.expected index e204b5a39264..430296c7c012 100644 --- a/rust/ql/test/query-tests/security/CWE-020/RegexInjection.expected +++ b/rust/ql/test/query-tests/security/CWE-020/RegexInjection.expected @@ -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 | diff --git a/rust/ql/test/query-tests/security/CWE-022/TaintedPath.expected b/rust/ql/test/query-tests/security/CWE-022/TaintedPath.expected new file mode 100644 index 000000000000..d85eed692f6b --- /dev/null +++ b/rust/ql/test/query-tests/security/CWE-022/TaintedPath.expected @@ -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; ::unwrap; Argument[self].Field[crate::result::Result::Ok(0)]; ReturnValue; value | +| 3 | Summary: lang:std; ::join; Argument[0]; ReturnValue; taint | +| 4 | Summary: lang:std; ::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 diff --git a/rust/ql/test/query-tests/security/CWE-022/TaintedPath.qlref b/rust/ql/test/query-tests/security/CWE-022/TaintedPath.qlref new file mode 100644 index 000000000000..2d4a275c02ac --- /dev/null +++ b/rust/ql/test/query-tests/security/CWE-022/TaintedPath.qlref @@ -0,0 +1,4 @@ +query: queries/security/CWE-022/TaintedPath.ql +postprocess: + - utils/test/PrettyPrintModels.ql + - utils/test/InlineExpectationsTestQuery.ql diff --git a/rust/ql/test/query-tests/security/CWE-022/TaintedPathSinks.expected b/rust/ql/test/query-tests/security/CWE-022/TaintedPathSinks.expected new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/rust/ql/test/query-tests/security/CWE-022/TaintedPathSinks.ql b/rust/ql/test/query-tests/security/CWE-022/TaintedPathSinks.ql new file mode 100644 index 000000000000..66345376de7b --- /dev/null +++ b/rust/ql/test/query-tests/security/CWE-022/TaintedPathSinks.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 diff --git a/rust/ql/test/query-tests/security/CWE-022/options.yml b/rust/ql/test/query-tests/security/CWE-022/options.yml new file mode 100644 index 000000000000..5277d967cc09 --- /dev/null +++ b/rust/ql/test/query-tests/security/CWE-022/options.yml @@ -0,0 +1,2 @@ +qltest_dependencies: + - poem = { version = "3.1.7" } diff --git a/rust/ql/test/query-tests/security/CWE-022/rust-toolchain.toml b/rust/ql/test/query-tests/security/CWE-022/rust-toolchain.toml new file mode 100644 index 000000000000..bd988b083968 --- /dev/null +++ b/rust/ql/test/query-tests/security/CWE-022/rust-toolchain.toml @@ -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 = [ ] diff --git a/rust/ql/test/query-tests/security/CWE-022/src/main.rs b/rust/ql/test/query-tests/security/CWE-022/src/main.rs new file mode 100644 index 000000000000..7c13da08db50 --- /dev/null +++ b/rust/ql/test/query-tests/security/CWE-022/src/main.rs @@ -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, // $ Source=remote1 +) -> Result { + 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) -> Result { + // 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) -> Result { + 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, // $ Source=remote4 +) -> Result { + 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, // $ Source=remote5 +) -> Result { + 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() {}