mirror of
https://github.com/github/codeql.git
synced 2025-12-20 18:56:32 +01:00
Merge remote-tracking branch 'upstream/main' into 'rc/3.14'
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
---
|
||||
category: minorAnalysis
|
||||
---
|
||||
* A number of Python queries now support sinks defined using data extensions. The format of data extensions for Python has been documented.
|
||||
@@ -35,6 +35,7 @@ private import semmle.python.frameworks.Idna
|
||||
private import semmle.python.frameworks.Invoke
|
||||
private import semmle.python.frameworks.Jmespath
|
||||
private import semmle.python.frameworks.Joblib
|
||||
private import semmle.python.frameworks.JsonPickle
|
||||
private import semmle.python.frameworks.Ldap
|
||||
private import semmle.python.frameworks.Ldap3
|
||||
private import semmle.python.frameworks.Libtaxii
|
||||
@@ -48,7 +49,9 @@ private import semmle.python.frameworks.Numpy
|
||||
private import semmle.python.frameworks.Opml
|
||||
private import semmle.python.frameworks.Oracledb
|
||||
private import semmle.python.frameworks.Pandas
|
||||
private import semmle.python.frameworks.Paramiko
|
||||
private import semmle.python.frameworks.Peewee
|
||||
private import semmle.python.frameworks.Pexpect
|
||||
private import semmle.python.frameworks.Phoenixdb
|
||||
private import semmle.python.frameworks.Psycopg
|
||||
private import semmle.python.frameworks.Psycopg2
|
||||
@@ -71,6 +74,7 @@ private import semmle.python.frameworks.SqlAlchemy
|
||||
private import semmle.python.frameworks.Starlette
|
||||
private import semmle.python.frameworks.Stdlib
|
||||
private import semmle.python.frameworks.Toml
|
||||
private import semmle.python.frameworks.Torch
|
||||
private import semmle.python.frameworks.Tornado
|
||||
private import semmle.python.frameworks.Twisted
|
||||
private import semmle.python.frameworks.Ujson
|
||||
|
||||
@@ -564,7 +564,6 @@ predicate neverSkipInPathGraph(Node n) {
|
||||
* Holds if `t1` and `t2` are compatible, that is, whether data can flow from
|
||||
* a node of type `t1` to a node of type `t2`.
|
||||
*/
|
||||
pragma[inline]
|
||||
predicate compatibleTypes(DataFlowType t1, DataFlowType t2) { any() }
|
||||
|
||||
predicate typeStrongerThan(DataFlowType t1, DataFlowType t2) { none() }
|
||||
@@ -576,8 +575,7 @@ predicate localMustFlowStep(Node nodeFrom, Node nodeTo) { none() }
|
||||
*/
|
||||
DataFlowType getNodeType(Node node) {
|
||||
result = TAnyFlow() and
|
||||
// Suppress unused variable warning
|
||||
node = node
|
||||
exists(node)
|
||||
}
|
||||
|
||||
/** Gets a string representation of a type returned by `getErasedRepr`. */
|
||||
|
||||
@@ -29,8 +29,8 @@ private module FabricV1 {
|
||||
// -------------------------------------------------------------------------
|
||||
// fabric.api
|
||||
// -------------------------------------------------------------------------
|
||||
/** Gets a reference to the `fabric.api` module. */
|
||||
API::Node api() { result = fabric().getMember("api") }
|
||||
/** Gets a reference to the `fabric.api` module. Also known as `fabric.operations` */
|
||||
API::Node api() { result = fabric().getMember(["api", "operations"]) }
|
||||
|
||||
/** Provides models for the `fabric.api` module */
|
||||
module Api {
|
||||
@@ -71,7 +71,7 @@ private module FabricV1 {
|
||||
* Provides classes modeling security-relevant aspects of the `fabric` PyPI package, for
|
||||
* version 2.x.
|
||||
*
|
||||
* See http://docs.fabfile.org/en/2.5/getting-st arted.html.
|
||||
* See http://docs.fabfile.org/en/2.5/getting-started.html.
|
||||
*/
|
||||
module FabricV2 {
|
||||
/** Gets a reference to the `fabric` module. */
|
||||
@@ -93,7 +93,9 @@ module FabricV2 {
|
||||
* See https://docs.fabfile.org/en/2.5/api/connection.html#fabric.connection.Connection.
|
||||
*/
|
||||
module ConnectionClass {
|
||||
/** Gets a reference to the `fabric.connection.Connection` class. */
|
||||
/**
|
||||
* Gets a reference to the `fabric.connection.Connection` class.
|
||||
*/
|
||||
API::Node classRef() {
|
||||
result = fabric().getMember("Connection")
|
||||
or
|
||||
@@ -109,40 +111,16 @@ module FabricV2 {
|
||||
* This can include instantiations of the class, return values from function
|
||||
* calls, or a special parameter that will be set when functions are called by an external
|
||||
* library.
|
||||
*
|
||||
* Use the predicate `Connection::instance()` to get references to instances of `fabric.connection.Connection`.
|
||||
*/
|
||||
abstract class InstanceSource extends DataFlow::LocalSourceNode { }
|
||||
|
||||
private class ClassInstantiation extends InstanceSource, DataFlow::CallCfgNode {
|
||||
ClassInstantiation() { this = classRef().getACall() }
|
||||
abstract class Instance extends API::Node {
|
||||
override string toString() { result = this.(API::Node).toString() }
|
||||
}
|
||||
|
||||
/** Gets a reference to an instance of `fabric.connection.Connection`. */
|
||||
private DataFlow::TypeTrackingNode instance(DataFlow::TypeTracker t) {
|
||||
t.start() and
|
||||
result instanceof InstanceSource
|
||||
or
|
||||
exists(DataFlow::TypeTracker t2 | result = instance(t2).track(t2, t))
|
||||
}
|
||||
|
||||
/** Gets a reference to an instance of `fabric.connection.Connection`. */
|
||||
DataFlow::Node instance() { instance(DataFlow::TypeTracker::end()).flowsTo(result) }
|
||||
|
||||
/**
|
||||
* Gets a reference to either `run`, `sudo`, or `local` method on a
|
||||
* `fabric.connection.Connection` instance.
|
||||
*
|
||||
* See
|
||||
* - https://docs.fabfile.org/en/2.5/api/connection.html#fabric.connection.Connection.run
|
||||
* - https://docs.fabfile.org/en/2.5/api/connection.html#fabric.connection.Connection.sudo
|
||||
* - https://docs.fabfile.org/en/2.5/api/connection.html#fabric.connection.Connection.local
|
||||
* A reference to the `fabric.connection.Connection` class.
|
||||
*/
|
||||
private DataFlow::TypeTrackingNode instanceRunMethods(DataFlow::TypeTracker t) {
|
||||
t.startInAttr(["run", "sudo", "local"]) and
|
||||
result = instance()
|
||||
or
|
||||
exists(DataFlow::TypeTracker t2 | result = instanceRunMethods(t2).track(t2, t))
|
||||
class ClassInstantiation extends Instance {
|
||||
ClassInstantiation() { this = classRef().getReturn() }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -154,8 +132,8 @@ module FabricV2 {
|
||||
* - https://docs.fabfile.org/en/2.5/api/connection.html#fabric.connection.Connection.sudo
|
||||
* - https://docs.fabfile.org/en/2.5/api/connection.html#fabric.connection.Connection.local
|
||||
*/
|
||||
DataFlow::Node instanceRunMethods() {
|
||||
instanceRunMethods(DataFlow::TypeTracker::end()).flowsTo(result)
|
||||
API::CallNode instanceRunMethods() {
|
||||
result = any(Instance is).getMember(["run", "sudo", "local"]).getACall()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -168,19 +146,38 @@ module FabricV2 {
|
||||
* - https://docs.fabfile.org/en/2.5/api/connection.html#fabric.connection.Connection.local
|
||||
*/
|
||||
private class FabricConnectionRunSudoLocalCall extends SystemCommandExecution::Range,
|
||||
DataFlow::CallCfgNode
|
||||
API::CallNode
|
||||
{
|
||||
FabricConnectionRunSudoLocalCall() {
|
||||
this.getFunction() = Fabric::Connection::ConnectionClass::instanceRunMethods()
|
||||
this = Fabric::Connection::ConnectionClass::instanceRunMethods()
|
||||
}
|
||||
|
||||
override DataFlow::Node getCommand() {
|
||||
result = [this.getArg(0), this.getArgByName("command")]
|
||||
}
|
||||
override DataFlow::Node getCommand() { result = this.getParameter(0, "command").asSink() }
|
||||
|
||||
override predicate isShellInterpreted(DataFlow::Node arg) { arg = this.getCommand() }
|
||||
}
|
||||
|
||||
/**
|
||||
* A `gateway` parameter of `fabric.connection.Connection` instance is considered as ssh proxy_command option and can execute command.
|
||||
* See https://docs.fabfile.org/en/latest/api/connection.html#fabric.connection.Connection
|
||||
*/
|
||||
private class FabricConnectionProxyCommand extends SystemCommandExecution::Range, API::CallNode {
|
||||
FabricConnectionProxyCommand() {
|
||||
this = Fabric::Connection::ConnectionClass::classRef().getACall() and
|
||||
// we want to make sure that the connection is established otherwise the command of proxy_command won't run.
|
||||
exists(
|
||||
this.getAMethodCall([
|
||||
"run", "get", "sudo", "open_gateway", "open", "create_session", "forward_local",
|
||||
"forward_remote", "put", "shell", "sftp"
|
||||
])
|
||||
)
|
||||
}
|
||||
|
||||
override DataFlow::Node getCommand() { result = this.getParameter(4, "gateway").asSink() }
|
||||
|
||||
override predicate isShellInterpreted(DataFlow::Node arg) { none() }
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// fabric.tasks
|
||||
// -------------------------------------------------------------------------
|
||||
@@ -193,14 +190,10 @@ module FabricV2 {
|
||||
API::Node task() { result in [tasks().getMember("task"), fabric().getMember("task")] }
|
||||
}
|
||||
|
||||
class FabricTaskFirstParamConnectionInstance extends Fabric::Connection::ConnectionClass::InstanceSource,
|
||||
DataFlow::ParameterNode
|
||||
class FabricTaskFirstParamConnectionInstance extends Fabric::Connection::ConnectionClass::Instance
|
||||
{
|
||||
FabricTaskFirstParamConnectionInstance() {
|
||||
exists(Function func |
|
||||
func.getADecorator() = Fabric::Tasks::task().getAValueReachableFromSource().asExpr() and
|
||||
this.getParameter() = func.getArg(0)
|
||||
)
|
||||
this = Fabric::Tasks::task().getParameter(0).getParameter(0)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -242,11 +235,14 @@ module FabricV2 {
|
||||
API::Node subclassInstance() { result = any(ModeledSubclass m).getASubclass*().getReturn() }
|
||||
|
||||
/**
|
||||
* Gets a reference to the `run` method on an instance of a subclass of `fabric.group.Group`.
|
||||
* Gets a reference to the `run` and `sudo` methods on an instance of a subclass of `fabric.group.Group`.
|
||||
*
|
||||
* See https://docs.fabfile.org/en/2.5/api/group.html#fabric.group.Group.run
|
||||
* See https://docs.fabfile.org/en/latest/api/group.html#fabric.group.Group.sudo
|
||||
*/
|
||||
API::Node subclassInstanceRunMethod() { result = subclassInstance().getMember("run") }
|
||||
API::Node subclassInstanceRunMethod() {
|
||||
result = subclassInstance().getMember(["run", "sudo"])
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -254,14 +250,12 @@ module FabricV2 {
|
||||
*
|
||||
* See https://docs.fabfile.org/en/2.5/api/group.html#fabric.group.Group.run
|
||||
*/
|
||||
private class FabricGroupRunCall extends SystemCommandExecution::Range, DataFlow::CallCfgNode {
|
||||
private class FabricGroupRunCall extends SystemCommandExecution::Range, API::CallNode {
|
||||
FabricGroupRunCall() {
|
||||
this = Fabric::Group::GroupClass::subclassInstanceRunMethod().getACall()
|
||||
}
|
||||
|
||||
override DataFlow::Node getCommand() {
|
||||
result = [this.getArg(0), this.getArgByName("command")]
|
||||
}
|
||||
override DataFlow::Node getCommand() { result = this.getParameter(0, "command").asSink() }
|
||||
|
||||
override predicate isShellInterpreted(DataFlow::Node arg) { arg = this.getCommand() }
|
||||
}
|
||||
|
||||
31
python/ql/lib/semmle/python/frameworks/JsonPickle.qll
Normal file
31
python/ql/lib/semmle/python/frameworks/JsonPickle.qll
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Provides classes modeling security-relevant aspects of the `jsonpickle` PyPI package.
|
||||
* See https://pypi.org/project/jsonpickle/.
|
||||
*/
|
||||
|
||||
private import python
|
||||
private import semmle.python.dataflow.new.RemoteFlowSources
|
||||
private import semmle.python.Concepts
|
||||
private import semmle.python.ApiGraphs
|
||||
|
||||
/**
|
||||
* Provides models for the `jsonpickle` PyPI package.
|
||||
* See https://pypi.org/project/jsonpickle/.
|
||||
*/
|
||||
private module Jsonpickle {
|
||||
/**
|
||||
* A Call to `jsonpickle.decode`.
|
||||
* See https://jsonpickle.readthedocs.io/en/latest/api.html#jsonpickle.decode
|
||||
*/
|
||||
private class JsonpickleDecode extends Decoding::Range, API::CallNode {
|
||||
JsonpickleDecode() { this = API::moduleImport("jsonpickle").getMember("decode").getACall() }
|
||||
|
||||
override predicate mayExecuteInput() { any() }
|
||||
|
||||
override DataFlow::Node getAnInput() { result = this.getParameter(0, "string").asSink() }
|
||||
|
||||
override DataFlow::Node getOutput() { result = this }
|
||||
|
||||
override string getFormat() { result = "pickle" }
|
||||
}
|
||||
}
|
||||
@@ -34,4 +34,121 @@ private module Pandas {
|
||||
|
||||
override string getFormat() { result = "pickle" }
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides security related models for `pandas.DataFrame`.
|
||||
* See https://pandas.pydata.org/docs/reference/frame.html
|
||||
*/
|
||||
module DataFrame {
|
||||
/**
|
||||
* A `pandas.DataFrame` Object.
|
||||
*
|
||||
* Extend this class to model new APIs.
|
||||
* See https://pandas.pydata.org/docs/reference/frame.html
|
||||
*/
|
||||
abstract class DataFrame extends API::Node {
|
||||
override string toString() { result = this.(API::Node).toString() }
|
||||
}
|
||||
|
||||
/**
|
||||
* A `pandas.DataFrame` instantiation.
|
||||
* See https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.html
|
||||
*/
|
||||
class DataFrameConstructor extends DataFrame {
|
||||
DataFrameConstructor() {
|
||||
this = API::moduleImport("pandas").getMember("DataFrame").getReturn()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The `pandas.read_*` functions that return a `pandas.DataFrame`.
|
||||
* See https://pandas.pydata.org/docs/reference/io.html
|
||||
*/
|
||||
class InputRead extends DataFrame {
|
||||
InputRead() {
|
||||
this =
|
||||
API::moduleImport("pandas")
|
||||
.getMember([
|
||||
"read_csv", "read_fwf", "read_pickle", "read_table", "read_clipboard",
|
||||
"read_excel", "read_xml", "read_parquet", "read_orc", "read_spss",
|
||||
"read_sql_table", "read_sql_query", "read_sql", "read_gbq", "read_stata"
|
||||
])
|
||||
.getReturn()
|
||||
or
|
||||
this = API::moduleImport("pandas").getMember("read_html").getReturn().getASubscript()
|
||||
or
|
||||
exists(API::Node readSas, API::CallNode readSasCall |
|
||||
readSas = API::moduleImport("pandas").getMember("read_sas") and
|
||||
this = readSas.getReturn() and
|
||||
readSasCall = readSas.getACall()
|
||||
|
|
||||
// Returns DataFrame if iterator=False and chunksize=None, Also with default values it returns DataFrame.
|
||||
(
|
||||
not readSasCall.getParameter(5, "iterator").asSink().asExpr().(BooleanLiteral)
|
||||
instanceof True
|
||||
or
|
||||
not exists(readSasCall.getParameter(5, "iterator").asSink())
|
||||
) and
|
||||
not exists(
|
||||
readSasCall.getParameter(4, "chunksize").asSink().asExpr().(IntegerLiteral).getN()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The `pandas.DataFrame.*` methods that return a `pandas.DataFrame` object.
|
||||
* See https://pandas.pydata.org/docs/reference/io.html
|
||||
*/
|
||||
class DataFrameMethods extends DataFrame {
|
||||
DataFrameMethods() {
|
||||
this =
|
||||
any(DataFrame df)
|
||||
.getMember([
|
||||
"copy", "from_records", "from_dict", "from_spmatrix", "assign", "select_dtypes",
|
||||
"set_flags", "astype", "infer_objects", "head", "xs", "get", "isin", "where",
|
||||
"mask", "query", "add", "mul", "truediv", "mod", "pow", "dot", "radd", "rsub",
|
||||
"rdiv", "rfloordiv", "rtruediv", "rpow", "lt", "gt", "le", "ne", "agg", "combine",
|
||||
"apply", "aggregate", "transform", "all", "any", "clip", "corr", "cov", "cummax",
|
||||
"cummin", "cumprod", "describe", "mode", "pct_change", "quantile", "rank",
|
||||
"round", "sem", "add_prefix", "add_suffix", "at_time", "between_time", "drop",
|
||||
"drop_duplicates", "filter", "first", "head", "idxmin", "last", "reindex",
|
||||
"reindex_like", "reset_index", "sample", "set_axis", "tail", "take", "truncate",
|
||||
"bfill", "dropna", "ffill", "fillna", "interpolate", "isna", "isnull", "notna",
|
||||
"notnull", "pad", "replace", "droplevel", "pivot", "pivot_table",
|
||||
"reorder_levels", "sort_values", "sort_index", "nlargest", "nsmallest",
|
||||
"swaplevel", "stack", "unstack", "isnull", "notna", "notnull", "replace",
|
||||
"droplevel", "pivot", "pivot_table", "reorder_levels", "sort_values",
|
||||
"sort_index", "nlargest", "nsmallest", "swaplevel", "stack", "unstack", "melt",
|
||||
"explode", "squeeze", "T", "transpose", "compare", "join", "from_spmatrix",
|
||||
"shift", "asof", "merge", "from_dict", "tz_convert", "to_period", "asfreq",
|
||||
"to_dense", "tz_localize", "box", "__dataframe__"
|
||||
])
|
||||
.getReturn()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A Call to `pandas.DataFrame.query` or `pandas.DataFrame.eval`.
|
||||
* See https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.query.html
|
||||
* https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.eval.html
|
||||
*/
|
||||
class CodeExecutionCall extends CodeExecution::Range, API::CallNode {
|
||||
CodeExecutionCall() {
|
||||
this = any(DataFrame::DataFrame df).getMember(["query", "eval"]).getACall()
|
||||
}
|
||||
|
||||
override DataFlow::Node getCode() { result = this.getParameter(0, "expr").asSink() }
|
||||
}
|
||||
|
||||
/**
|
||||
* A Call to `pandas.eval`.
|
||||
* See https://pandas.pydata.org/docs/reference/api/pandas.eval.html
|
||||
*/
|
||||
class PandasEval extends CodeExecution::Range, API::CallNode {
|
||||
PandasEval() { this = API::moduleImport("pandas").getMember("eval").getACall() }
|
||||
|
||||
override DataFlow::Node getCode() { result = this.getParameter(0, "expr").asSink() }
|
||||
}
|
||||
}
|
||||
|
||||
32
python/ql/lib/semmle/python/frameworks/Paramiko.qll
Normal file
32
python/ql/lib/semmle/python/frameworks/Paramiko.qll
Normal file
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Provides classes modeling security-relevant aspects of the `paramiko` PyPI package.
|
||||
* See https://pypi.org/project/paramiko/.
|
||||
*/
|
||||
|
||||
private import python
|
||||
private import semmle.python.dataflow.new.RemoteFlowSources
|
||||
private import semmle.python.Concepts
|
||||
private import semmle.python.ApiGraphs
|
||||
|
||||
/**
|
||||
* Provides models for the `paramiko` PyPI package.
|
||||
* See https://pypi.org/project/paramiko/.
|
||||
*/
|
||||
private module Paramiko {
|
||||
/**
|
||||
* The first argument of `paramiko.ProxyCommand`.
|
||||
*
|
||||
* the `paramiko.ProxyCommand` is equivalent of `ssh -o ProxyCommand="CMD"`
|
||||
* which runs `CMD` on the local system.
|
||||
* See https://paramiko.pydata.org/docs/reference/api/paramiko.eval.html
|
||||
*/
|
||||
class ParamikoProxyCommand extends SystemCommandExecution::Range, API::CallNode {
|
||||
ParamikoProxyCommand() {
|
||||
this = API::moduleImport("paramiko").getMember("ProxyCommand").getACall()
|
||||
}
|
||||
|
||||
override DataFlow::Node getCommand() { result = this.getParameter(0, "command_line").asSink() }
|
||||
|
||||
override predicate isShellInterpreted(DataFlow::Node arg) { none() }
|
||||
}
|
||||
}
|
||||
45
python/ql/lib/semmle/python/frameworks/Pexpect.qll
Normal file
45
python/ql/lib/semmle/python/frameworks/Pexpect.qll
Normal file
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Provides classes modeling security-relevant aspects of the `pexpect` PyPI package.
|
||||
* See https://pypi.org/project/pexpect/.
|
||||
*/
|
||||
|
||||
private import python
|
||||
private import semmle.python.dataflow.new.RemoteFlowSources
|
||||
private import semmle.python.Concepts
|
||||
private import semmle.python.ApiGraphs
|
||||
|
||||
/**
|
||||
* Provides models for the `pexpect` PyPI package.
|
||||
* See https://pypi.org/project/pexpect/.
|
||||
*/
|
||||
private module Pexpect {
|
||||
/**
|
||||
* The calls to `pexpect.*` functions that execute commands
|
||||
* See https://pexpect.readthedocs.io/en/stable/api/pexpect.html#pexpect.spawn
|
||||
* See https://pexpect.readthedocs.io/en/stable/api/pexpect.html#pexpect.run
|
||||
*/
|
||||
class PexpectCommandExec extends SystemCommandExecution::Range, API::CallNode {
|
||||
PexpectCommandExec() {
|
||||
this = API::moduleImport("pexpect").getMember(["run", "runu", "spawn", "spawnu"]).getACall()
|
||||
}
|
||||
|
||||
override DataFlow::Node getCommand() { result = this.getParameter(0, "command").asSink() }
|
||||
|
||||
override predicate isShellInterpreted(DataFlow::Node arg) { none() }
|
||||
}
|
||||
|
||||
/**
|
||||
* A call to `pexpect.popen_spawn.PopenSpawn`
|
||||
* See https://pexpect.readthedocs.io/en/stable/api/popen_spawn.html#pexpect.popen_spawn.PopenSpawn
|
||||
*/
|
||||
class PexpectPopenSpawn extends SystemCommandExecution::Range, API::CallNode {
|
||||
PexpectPopenSpawn() {
|
||||
this =
|
||||
API::moduleImport("pexpect").getMember("popen_spawn").getMember("PopenSpawn").getACall()
|
||||
}
|
||||
|
||||
override DataFlow::Node getCommand() { result = this.getParameter(0, "cmd").asSink() }
|
||||
|
||||
override predicate isShellInterpreted(DataFlow::Node arg) { none() }
|
||||
}
|
||||
}
|
||||
72
python/ql/lib/semmle/python/frameworks/Torch.qll
Normal file
72
python/ql/lib/semmle/python/frameworks/Torch.qll
Normal file
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* Provides classes modeling security-relevant aspects of the `torch` PyPI package.
|
||||
* See https://pypi.org/project/torch/.
|
||||
*/
|
||||
|
||||
private import python
|
||||
private import semmle.python.Concepts
|
||||
private import semmle.python.ApiGraphs
|
||||
|
||||
/**
|
||||
* Provides models for the `torch` PyPI package.
|
||||
* See https://pypi.org/project/torch/.
|
||||
*/
|
||||
private module Torch {
|
||||
/**
|
||||
* A call to `torch.load`
|
||||
* See https://pytorch.org/docs/stable/generated/torch.load.html#torch.load
|
||||
*/
|
||||
private class TorchLoadCall extends Decoding::Range, API::CallNode {
|
||||
TorchLoadCall() { this = API::moduleImport("torch").getMember("load").getACall() }
|
||||
|
||||
override predicate mayExecuteInput() {
|
||||
not exists(this.getParameter(2, "pickle_module").asSink()) or
|
||||
this.getParameter(2, "pickle_module").asSink().asExpr() instanceof None
|
||||
}
|
||||
|
||||
override DataFlow::Node getAnInput() { result = this.getParameter(0, "f").asSink() }
|
||||
|
||||
override DataFlow::Node getOutput() { result = this }
|
||||
|
||||
override string getFormat() { result = "pickle" }
|
||||
}
|
||||
|
||||
/**
|
||||
* A call to `torch.package.PackageImporter`
|
||||
* See https://pytorch.org/docs/stable/package.html#torch.package.PackageImporter
|
||||
*/
|
||||
private class TorchPackageImporter extends Decoding::Range, API::CallNode {
|
||||
TorchPackageImporter() {
|
||||
this = API::moduleImport("torch").getMember("package").getMember("PackageImporter").getACall() and
|
||||
exists(this.getAMethodCall("load_pickle"))
|
||||
}
|
||||
|
||||
override predicate mayExecuteInput() { any() }
|
||||
|
||||
override DataFlow::Node getAnInput() {
|
||||
result = this.getParameter(0, "file_or_buffer").asSink()
|
||||
}
|
||||
|
||||
override DataFlow::Node getOutput() { result = this.getAMethodCall("load_pickle") }
|
||||
|
||||
override string getFormat() { result = "pickle" }
|
||||
}
|
||||
|
||||
/**
|
||||
* A call to `torch.jit.load`
|
||||
* See https://pytorch.org/docs/stable/generated/torch.jit.load.html#torch.jit.load
|
||||
*/
|
||||
private class TorchJitLoad extends Decoding::Range, API::CallNode {
|
||||
TorchJitLoad() {
|
||||
this = API::moduleImport("torch").getMember("jit").getMember("load").getACall()
|
||||
}
|
||||
|
||||
override predicate mayExecuteInput() { any() }
|
||||
|
||||
override DataFlow::Node getAnInput() { result = this.getParameter(0, "f").asSink() }
|
||||
|
||||
override DataFlow::Node getOutput() { result = this }
|
||||
|
||||
override string getFormat() { result = "pickle" }
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ private import semmle.python.dataflow.new.DataFlow
|
||||
private import semmle.python.Concepts
|
||||
private import semmle.python.dataflow.new.RemoteFlowSources
|
||||
private import semmle.python.dataflow.new.BarrierGuards
|
||||
private import semmle.python.frameworks.data.ModelsAsData
|
||||
|
||||
/**
|
||||
* Provides default sources, sinks and sanitizers for detecting
|
||||
@@ -43,6 +44,10 @@ module CodeInjection {
|
||||
CodeExecutionAsSink() { this = any(CodeExecution e).getCode() }
|
||||
}
|
||||
|
||||
private class SinkFromModel extends Sink {
|
||||
SinkFromModel() { this = ModelOutput::getASinkNode("code-injection").asSink() }
|
||||
}
|
||||
|
||||
/**
|
||||
* A comparison with a constant string, considered as a sanitizer-guard.
|
||||
*/
|
||||
|
||||
@@ -9,6 +9,7 @@ private import semmle.python.dataflow.new.DataFlow
|
||||
private import semmle.python.Concepts
|
||||
private import semmle.python.dataflow.new.RemoteFlowSources
|
||||
private import semmle.python.dataflow.new.BarrierGuards
|
||||
private import semmle.python.frameworks.data.ModelsAsData
|
||||
|
||||
/**
|
||||
* Provides default sources, sinks and sanitizers for detecting
|
||||
@@ -78,6 +79,10 @@ module CommandInjection {
|
||||
}
|
||||
}
|
||||
|
||||
private class SinkFromModel extends Sink {
|
||||
SinkFromModel() { this = ModelOutput::getASinkNode("command-injection").asSink() }
|
||||
}
|
||||
|
||||
/**
|
||||
* A comparison with a constant string, considered as a sanitizer-guard.
|
||||
*/
|
||||
|
||||
@@ -9,6 +9,7 @@ private import semmle.python.dataflow.new.DataFlow
|
||||
private import semmle.python.Concepts
|
||||
private import semmle.python.dataflow.new.RemoteFlowSources
|
||||
private import semmle.python.dataflow.new.BarrierGuards
|
||||
private import semmle.python.frameworks.data.ModelsAsData
|
||||
|
||||
/**
|
||||
* Provides default sources, sinks and sanitizers for detecting
|
||||
@@ -71,6 +72,10 @@ module LogInjection {
|
||||
}
|
||||
}
|
||||
|
||||
private class SinkFromModel extends Sink {
|
||||
SinkFromModel() { this = ModelOutput::getASinkNode("log-injection").asSink() }
|
||||
}
|
||||
|
||||
/**
|
||||
* A comparison with a constant string, considered as a sanitizer-guard.
|
||||
*/
|
||||
|
||||
@@ -9,6 +9,7 @@ private import semmle.python.dataflow.new.DataFlow
|
||||
private import semmle.python.Concepts
|
||||
private import semmle.python.dataflow.new.RemoteFlowSources
|
||||
private import semmle.python.dataflow.new.BarrierGuards
|
||||
private import semmle.python.ApiGraphs
|
||||
|
||||
/**
|
||||
* Provides default sources, sinks and sanitizers for detecting
|
||||
@@ -137,4 +138,34 @@ module ServerSideRequestForgery {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** A validation that a string does not contain certain characters, considered as a sanitizer. */
|
||||
private class StringRestrictionSanitizerGuard extends Sanitizer {
|
||||
StringRestrictionSanitizerGuard() {
|
||||
this = DataFlow::BarrierGuard<stringRestriction/3>::getABarrierNode()
|
||||
}
|
||||
}
|
||||
|
||||
private predicate stringRestriction(DataFlow::GuardNode g, ControlFlowNode node, boolean branch) {
|
||||
exists(DataFlow::MethodCallNode call, DataFlow::Node strNode |
|
||||
call.asCfgNode() = g and strNode.asCfgNode() = node
|
||||
|
|
||||
branch = true and
|
||||
call.calls(strNode,
|
||||
["isalnum", "isalpha", "isdecimal", "isdigit", "isidentifier", "isnumeric", "isspace"])
|
||||
or
|
||||
branch = true and
|
||||
call = API::moduleImport("re").getMember(["match", "fullmatch"]).getACall() and
|
||||
strNode = [call.getArg(1), call.getArgByName("string")]
|
||||
or
|
||||
branch = true and
|
||||
call =
|
||||
API::moduleImport("re")
|
||||
.getMember("compile")
|
||||
.getReturn()
|
||||
.getMember(["match", "fullmatch"])
|
||||
.getACall() and
|
||||
strNode = [call.getArg(0), call.getArgByName("string")]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ private import semmle.python.dataflow.new.DataFlow
|
||||
private import semmle.python.Concepts
|
||||
private import semmle.python.dataflow.new.RemoteFlowSources
|
||||
private import semmle.python.dataflow.new.BarrierGuards
|
||||
private import semmle.python.frameworks.data.ModelsAsData
|
||||
|
||||
/**
|
||||
* Provides default sources, sinks and sanitizers for detecting
|
||||
@@ -48,6 +49,10 @@ module UnsafeDeserialization {
|
||||
}
|
||||
}
|
||||
|
||||
private class SinkFromModel extends Sink {
|
||||
SinkFromModel() { this = ModelOutput::getASinkNode("unsafe-deserialization").asSink() }
|
||||
}
|
||||
|
||||
/**
|
||||
* A comparison with a constant string, considered as a sanitizer-guard.
|
||||
*/
|
||||
|
||||
@@ -9,6 +9,7 @@ private import semmle.python.dataflow.new.DataFlow
|
||||
private import semmle.python.Concepts
|
||||
private import semmle.python.dataflow.new.RemoteFlowSources
|
||||
private import semmle.python.dataflow.new.BarrierGuards
|
||||
private import semmle.python.frameworks.data.ModelsAsData
|
||||
|
||||
/**
|
||||
* Provides default sources, sinks and sanitizers for detecting
|
||||
@@ -89,6 +90,10 @@ module UrlRedirect {
|
||||
}
|
||||
}
|
||||
|
||||
private class SinkFromModel extends Sink {
|
||||
SinkFromModel() { this = ModelOutput::getASinkNode("url-redirection").asSink() }
|
||||
}
|
||||
|
||||
/**
|
||||
* The right side of a string-concat, considered as a sanitizer.
|
||||
*/
|
||||
|
||||
@@ -12,6 +12,29 @@
|
||||
|
||||
import python
|
||||
import Variables.Definition
|
||||
import semmle.python.ApiGraphs
|
||||
|
||||
private predicate is_pytest_fixture(Import imp, Variable name) {
|
||||
exists(Alias a, API::Node pytest_fixture, API::Node decorator |
|
||||
pytest_fixture = API::moduleImport("pytest").getMember("fixture") and
|
||||
// The additional `.getReturn()` is to account for the difference between
|
||||
// ```
|
||||
// @pytest.fixture
|
||||
// def foo():
|
||||
// ...
|
||||
// ```
|
||||
// and
|
||||
// ```
|
||||
// @pytest.fixture(some, args, here)
|
||||
// def foo():
|
||||
// ...
|
||||
// ```
|
||||
decorator in [pytest_fixture, pytest_fixture.getReturn()] and
|
||||
a = imp.getAName() and
|
||||
a.getAsname().(Name).getVariable() = name and
|
||||
a.getValue() = decorator.getReturn().getAValueReachableFromSource().asExpr()
|
||||
)
|
||||
}
|
||||
|
||||
predicate global_name_used(Module m, string name) {
|
||||
exists(Name u, GlobalVariable v |
|
||||
@@ -117,6 +140,7 @@ predicate unused_import(Import imp, Variable name) {
|
||||
not all_not_understood(imp.getEnclosingModule()) and
|
||||
not imported_module_used_in_doctest(imp) and
|
||||
not imported_alias_used_in_typehint(imp, name) and
|
||||
not is_pytest_fixture(imp, name) and
|
||||
// Only consider import statements that actually point-to something (possibly an unknown module).
|
||||
// If this is not the case, it's likely that the import statement never gets executed.
|
||||
imp.getAName().getValue().pointsTo(_)
|
||||
|
||||
4
python/ql/src/change-notes/2024-06-04-ssrf-sanitizers.md
Normal file
4
python/ql/src/change-notes/2024-06-04-ssrf-sanitizers.md
Normal file
@@ -0,0 +1,4 @@
|
||||
---
|
||||
category: minorAnalysis
|
||||
---
|
||||
* Additional sanitizers have been added to the `py/full-ssrf` and `py/partial-ssrf` queries for methods that verify a string contains only a certain set of characters, such as `.isalnum()` as well as regular expression tests.
|
||||
@@ -1,17 +0,0 @@
|
||||
<!DOCTYPE qhelp SYSTEM "qhelp.dtd">
|
||||
<qhelp>
|
||||
<overview>
|
||||
<p>
|
||||
Processing an unvalidated user input can allow an attacker to inject arbitrary command in your local and remote servers when creating a ssh connection.
|
||||
</p>
|
||||
</overview>
|
||||
<recommendation>
|
||||
<p>
|
||||
This vulnerability can be prevented by not allowing untrusted user input to be passed as ProxyCommand or exec_command.
|
||||
</p>
|
||||
</recommendation>
|
||||
<example>
|
||||
<p>In the example below, the ProxyCommand and exec_command are controlled by the user and hence leads to a vulnerability.</p>
|
||||
<sample src="paramikoBad.py" />
|
||||
</example>
|
||||
</qhelp>
|
||||
@@ -1,59 +0,0 @@
|
||||
/**
|
||||
* @name RCE with user provided command with paramiko ssh client
|
||||
* @description user provided command can lead to execute code on a external server that can be belong to other users or admins
|
||||
* @kind path-problem
|
||||
* @problem.severity error
|
||||
* @security-severity 9.3
|
||||
* @precision high
|
||||
* @id py/paramiko-command-injection
|
||||
* @tags security
|
||||
* experimental
|
||||
* external/cwe/cwe-074
|
||||
*/
|
||||
|
||||
import python
|
||||
import semmle.python.dataflow.new.DataFlow
|
||||
import semmle.python.dataflow.new.TaintTracking
|
||||
import semmle.python.dataflow.new.RemoteFlowSources
|
||||
import semmle.python.ApiGraphs
|
||||
|
||||
private API::Node paramikoClient() {
|
||||
result = API::moduleImport("paramiko").getMember("SSHClient").getReturn()
|
||||
}
|
||||
|
||||
private module ParamikoConfig implements DataFlow::ConfigSig {
|
||||
predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
|
||||
|
||||
/**
|
||||
* exec_command of `paramiko.SSHClient` class execute command on ssh target server
|
||||
* the `paramiko.ProxyCommand` is equivalent of `ssh -o ProxyCommand="CMD"`
|
||||
* and it run CMD on current system that running the ssh command
|
||||
* the Sink related to proxy command is the `connect` method of `paramiko.SSHClient` class
|
||||
*/
|
||||
predicate isSink(DataFlow::Node sink) {
|
||||
sink = paramikoClient().getMember("exec_command").getACall().getParameter(0, "command").asSink()
|
||||
or
|
||||
sink = paramikoClient().getMember("connect").getACall().getParameter(11, "sock").asSink()
|
||||
}
|
||||
|
||||
/**
|
||||
* this additional taint step help taint tracking to find the vulnerable `connect` method of `paramiko.SSHClient` class
|
||||
*/
|
||||
predicate isAdditionalFlowStep(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) {
|
||||
exists(API::CallNode call |
|
||||
call = API::moduleImport("paramiko").getMember("ProxyCommand").getACall() and
|
||||
nodeFrom = call.getParameter(0, "command_line").asSink() and
|
||||
nodeTo = call
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** Global taint-tracking for detecting "paramiko command injection" vulnerabilities. */
|
||||
module ParamikoFlow = TaintTracking::Global<ParamikoConfig>;
|
||||
|
||||
import ParamikoFlow::PathGraph
|
||||
|
||||
from ParamikoFlow::PathNode source, ParamikoFlow::PathNode sink
|
||||
where ParamikoFlow::flowPath(source, sink)
|
||||
select sink.getNode(), source, sink, "This code execution depends on a $@.", source.getNode(),
|
||||
"a user-provided value"
|
||||
@@ -0,0 +1,22 @@
|
||||
<!DOCTYPE qhelp SYSTEM "qhelp.dtd">
|
||||
<qhelp>
|
||||
<overview>
|
||||
<p>
|
||||
Allowing users to execute arbitrary commands using an SSH connection on a remote server can lead to security issues unless you implement proper authorization.
|
||||
</p>
|
||||
<p>
|
||||
Assume that you connect to a remote system via SSH connection from your main or local server that accepts user-controlled data and has interaction with users that you don't trust, passing these data to SSH API as a part of a command that will be executed on a secondary remote server can lead to security issues. You should consider proper authorization rules very carefully.
|
||||
</p>
|
||||
</overview>
|
||||
<recommendation>
|
||||
<p>
|
||||
This vulnerability can be prevented by implementing proper authorization rules for untrusted user input that can be passed to your secondary servers.
|
||||
</p>
|
||||
</recommendation>
|
||||
<example>
|
||||
<p>In the example below, the exec_command is controlled by the user and hence leads to a vulnerability.</p>
|
||||
<sample src="paramikoBad.py" />
|
||||
<p>In the example below, the exec_command is controlled by the an Authorized user and hence it is safe.</p>
|
||||
<sample src="paramikoGood.py" />
|
||||
</example>
|
||||
</qhelp>
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* @name Command execution on a secondary remote server
|
||||
* @description user provided command can lead to execute code on a external server that can be belong to other users or admins
|
||||
* @kind path-problem
|
||||
* @problem.severity error
|
||||
* @security-severity 9.3
|
||||
* @precision high
|
||||
* @id py/paramiko-command-injection
|
||||
* @tags security
|
||||
* experimental
|
||||
* external/cwe/cwe-074
|
||||
*/
|
||||
|
||||
import python
|
||||
import experimental.semmle.python.security.RemoteCommandExecution
|
||||
import RemoteCommandExecutionFlow::PathGraph
|
||||
|
||||
from RemoteCommandExecutionFlow::PathNode source, RemoteCommandExecutionFlow::PathNode sink
|
||||
where RemoteCommandExecutionFlow::flowPath(source, sink)
|
||||
select sink.getNode(), source, sink, "This code execution depends on a $@.", source.getNode(),
|
||||
"a user-provided value"
|
||||
@@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
from flask import request, Flask
|
||||
import paramiko
|
||||
from paramiko import SSHClient
|
||||
|
||||
app = Flask(__name__)
|
||||
paramiko_ssh_client = SSHClient()
|
||||
paramiko_ssh_client.load_system_host_keys()
|
||||
paramiko_ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
paramiko_ssh_client.connect(hostname="127.0.0.1", port="22", username="ssh_user_name", pkey="k", timeout=11, banner_timeout=200)
|
||||
|
||||
|
||||
@app.route('/external_exec_command_1')
|
||||
def withoutAuthorization():
|
||||
user_cmd = request.args.get('command')
|
||||
stdin, stdout, stderr = paramiko_ssh_client.exec_command(user_cmd)
|
||||
return stdout
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.debug = False
|
||||
app.run()
|
||||
|
||||
@@ -12,23 +12,14 @@ paramiko_ssh_client.connect(hostname="127.0.0.1", port="22", username="ssh_user_
|
||||
|
||||
|
||||
@app.route('/external_exec_command_1')
|
||||
def bad1():
|
||||
def withAuthorization():
|
||||
user_cmd = request.args.get('command')
|
||||
auth_jwt = request.args.get('Auth')
|
||||
# validating jwt token first
|
||||
# .... then continue to run the command
|
||||
stdin, stdout, stderr = paramiko_ssh_client.exec_command(user_cmd)
|
||||
return stdout
|
||||
|
||||
@app.route('/external_exec_command_2')
|
||||
def bad2():
|
||||
user_cmd = request.args.get('command')
|
||||
stdin, stdout, stderr = paramiko_ssh_client.exec_command(command=user_cmd)
|
||||
return stdout
|
||||
|
||||
|
||||
@app.route('/proxycommand')
|
||||
def bad2():
|
||||
user_cmd = request.args.get('command')
|
||||
stdin, stdout, stderr = paramiko_ssh_client.connect('hostname', username='user',password='yourpassword',sock=paramiko.ProxyCommand(user_cmd))
|
||||
return stdout
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.debug = False
|
||||
@@ -15,6 +15,39 @@ private import semmle.python.dataflow.new.TaintTracking
|
||||
private import experimental.semmle.python.Frameworks
|
||||
private import semmle.python.Concepts
|
||||
|
||||
/**
|
||||
* A data-flow node that executes an operating system command,
|
||||
* on a remote server likely by SSH connections.
|
||||
*
|
||||
* Extend this class to refine existing API models. If you want to model new APIs,
|
||||
* extend `RemoteCommandExecution::Range` instead.
|
||||
*/
|
||||
class RemoteCommandExecution extends DataFlow::Node instanceof RemoteCommandExecution::Range {
|
||||
/** Holds if a shell interprets `arg`. */
|
||||
predicate isShellInterpreted(DataFlow::Node arg) { super.isShellInterpreted(arg) }
|
||||
|
||||
/** Gets the argument that specifies the command to be executed. */
|
||||
DataFlow::Node getCommand() { result = super.getCommand() }
|
||||
}
|
||||
|
||||
/** Provides classes for modeling new remote server command execution APIs. */
|
||||
module RemoteCommandExecution {
|
||||
/**
|
||||
* A data-flow node that executes an operating system command,
|
||||
* on a remote server likely by SSH connections.
|
||||
*
|
||||
* Extend this class to model new APIs. If you want to refine existing API models,
|
||||
* extend `RemoteCommandExecution` instead.
|
||||
*/
|
||||
abstract class Range extends DataFlow::Node {
|
||||
/** Gets the argument that specifies the command to be executed. */
|
||||
abstract DataFlow::Node getCommand();
|
||||
|
||||
/** Holds if a shell interprets `arg`. */
|
||||
predicate isShellInterpreted(DataFlow::Node arg) { none() }
|
||||
}
|
||||
}
|
||||
|
||||
/** Provides classes for modeling copying file related APIs. */
|
||||
module CopyFile {
|
||||
/**
|
||||
|
||||
@@ -2,10 +2,16 @@
|
||||
* Helper file that imports all framework modeling.
|
||||
*/
|
||||
|
||||
private import experimental.semmle.python.frameworks.AsyncSsh
|
||||
private import experimental.semmle.python.frameworks.Stdlib
|
||||
private import experimental.semmle.python.frameworks.Flask
|
||||
private import experimental.semmle.python.frameworks.Django
|
||||
private import experimental.semmle.python.frameworks.LDAP
|
||||
private import experimental.semmle.python.frameworks.Netmiko
|
||||
private import experimental.semmle.python.frameworks.Paramiko
|
||||
private import experimental.semmle.python.frameworks.Pexpect
|
||||
private import experimental.semmle.python.frameworks.Scrapli
|
||||
private import experimental.semmle.python.frameworks.Twisted
|
||||
private import experimental.semmle.python.frameworks.JWT
|
||||
private import experimental.semmle.python.frameworks.Csv
|
||||
private import experimental.semmle.python.libraries.PyJWT
|
||||
@@ -14,5 +20,6 @@ private import experimental.semmle.python.libraries.Authlib
|
||||
private import experimental.semmle.python.libraries.PythonJose
|
||||
private import experimental.semmle.python.frameworks.CopyFile
|
||||
private import experimental.semmle.python.frameworks.Sendgrid
|
||||
private import experimental.semmle.python.frameworks.Ssh2
|
||||
private import experimental.semmle.python.libraries.FlaskMail
|
||||
private import experimental.semmle.python.libraries.SmtpLib
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Provides classes modeling security-relevant aspects of the `asyncssh` PyPI package.
|
||||
* See https://pypi.org/project/asyncssh/.
|
||||
*/
|
||||
|
||||
private import python
|
||||
private import semmle.python.dataflow.new.RemoteFlowSources
|
||||
private import semmle.python.Concepts
|
||||
private import semmle.python.ApiGraphs
|
||||
import experimental.semmle.python.Concepts
|
||||
|
||||
/**
|
||||
* Provides models for the `asyncssh` PyPI package.
|
||||
* See https://pypi.org/project/asyncssh/.
|
||||
*/
|
||||
private module Asyncssh {
|
||||
/**
|
||||
* Gets `asyncssh` package.
|
||||
*/
|
||||
private API::Node asyncssh() { result = API::moduleImport("asyncssh") }
|
||||
|
||||
/**
|
||||
* A `run` method responsible for executing commands on remote secondary servers.
|
||||
*/
|
||||
class AsyncsshRun extends RemoteCommandExecution::Range, API::CallNode {
|
||||
AsyncsshRun() { this = asyncssh().getMember("connect").getReturn().getMember("run").getACall() }
|
||||
|
||||
override DataFlow::Node getCommand() { result = this.getParameter(0, "command").asSink() }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* Provides classes modeling security-relevant aspects of the `netmiko` PyPI package.
|
||||
* See https://pypi.org/project/netmiko/.
|
||||
*/
|
||||
|
||||
private import python
|
||||
private import semmle.python.dataflow.new.RemoteFlowSources
|
||||
private import semmle.python.Concepts
|
||||
private import semmle.python.ApiGraphs
|
||||
import experimental.semmle.python.Concepts
|
||||
|
||||
/**
|
||||
* Provides models for the `netmiko` PyPI package.
|
||||
* See https://pypi.org/project/netmiko/.
|
||||
*/
|
||||
private module Netmiko {
|
||||
/**
|
||||
* Gets `netmiko` package.
|
||||
*/
|
||||
private API::Node netmiko() { result = API::moduleImport("netmiko") }
|
||||
|
||||
/**
|
||||
* Gets `netmiko.ConnectHandler` return value.
|
||||
*/
|
||||
private API::Node netmikoConnectHandler() {
|
||||
result = netmiko().getMember("ConnectHandler").getReturn()
|
||||
}
|
||||
|
||||
/**
|
||||
* The `send_*` methods responsible for executing commands on remote secondary servers.
|
||||
*/
|
||||
class NetmikoSendCommand extends RemoteCommandExecution::Range, API::CallNode {
|
||||
boolean isMultiline;
|
||||
|
||||
NetmikoSendCommand() {
|
||||
this =
|
||||
netmikoConnectHandler()
|
||||
.getMember(["send_command", "send_command_expect", "send_command_timing"])
|
||||
.getACall() and
|
||||
isMultiline = false
|
||||
or
|
||||
this =
|
||||
netmikoConnectHandler().getMember(["send_multiline", "send_multiline_timing"]).getACall() and
|
||||
isMultiline = true
|
||||
}
|
||||
|
||||
override DataFlow::Node getCommand() {
|
||||
result = this.getParameter(0, "command_string").asSink() and
|
||||
isMultiline = false
|
||||
or
|
||||
result = this.getParameter(0, "commands").asSink() and
|
||||
isMultiline = true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Provides classes modeling security-relevant aspects of the `paramiko` PyPI package.
|
||||
* See https://pypi.org/project/paramiko/.
|
||||
*/
|
||||
|
||||
private import python
|
||||
private import semmle.python.dataflow.new.RemoteFlowSources
|
||||
private import semmle.python.Concepts
|
||||
private import semmle.python.ApiGraphs
|
||||
import experimental.semmle.python.Concepts
|
||||
|
||||
/**
|
||||
* Provides models for the `paramiko` PyPI package.
|
||||
* See https://pypi.org/project/paramiko/.
|
||||
*/
|
||||
private module Paramiko {
|
||||
/**
|
||||
* Gets `paramiko` package.
|
||||
*/
|
||||
private API::Node paramiko() { result = API::moduleImport("paramiko") }
|
||||
|
||||
/**
|
||||
* Gets `paramiko.SSHClient` return value.
|
||||
*/
|
||||
private API::Node paramikoClient() { result = paramiko().getMember("SSHClient").getReturn() }
|
||||
|
||||
/**
|
||||
* The `exec_command` of `paramiko.SSHClient` class execute command on ssh target server
|
||||
*/
|
||||
class ParamikoExecCommand extends RemoteCommandExecution::Range, API::CallNode {
|
||||
ParamikoExecCommand() { this = paramikoClient().getMember("exec_command").getACall() }
|
||||
|
||||
override DataFlow::Node getCommand() { result = this.getParameter(0, "command").asSink() }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Provides classes modeling security-relevant aspects of the `pexpect` PyPI package.
|
||||
* See https://pypi.org/project/pexpect/.
|
||||
*/
|
||||
|
||||
private import python
|
||||
private import semmle.python.dataflow.new.RemoteFlowSources
|
||||
private import semmle.python.ApiGraphs
|
||||
import experimental.semmle.python.Concepts
|
||||
|
||||
/**
|
||||
* Provides models for the `pexpect` PyPI package.
|
||||
* See https://pypi.org/project/pexpect/.
|
||||
*/
|
||||
private module Pexpect {
|
||||
/**
|
||||
* The calls to `pexpect.pxssh.pxssh` functions that execute commands
|
||||
* See https://pexpect.readthedocs.io/en/stable/api/pxssh.html
|
||||
*/
|
||||
class PexpectCommandExec extends RemoteCommandExecution::Range, API::CallNode {
|
||||
PexpectCommandExec() {
|
||||
this =
|
||||
API::moduleImport("pexpect")
|
||||
.getMember("pxssh")
|
||||
.getMember("pxssh")
|
||||
.getReturn()
|
||||
.getMember(["send", "sendline"])
|
||||
.getACall()
|
||||
}
|
||||
|
||||
override DataFlow::Node getCommand() { result = this.getParameter(0, "s").asSink() }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* Provides classes modeling security-relevant aspects of the `scrapli` PyPI package.
|
||||
* See https://pypi.org/project/scrapli/.
|
||||
*/
|
||||
|
||||
private import python
|
||||
private import semmle.python.dataflow.new.RemoteFlowSources
|
||||
private import semmle.python.Concepts
|
||||
private import semmle.python.ApiGraphs
|
||||
import experimental.semmle.python.Concepts
|
||||
|
||||
/**
|
||||
* Provides models for the `scrapli` PyPI package.
|
||||
* See https://pypi.org/project/scrapli/.
|
||||
*/
|
||||
private module Scrapli {
|
||||
/**
|
||||
* Gets `scrapli` package.
|
||||
*/
|
||||
private API::Node scrapli() { result = API::moduleImport("scrapli") }
|
||||
|
||||
/**
|
||||
* Gets `scrapli.driver` package.
|
||||
*/
|
||||
private API::Node scrapliDriver() { result = scrapli().getMember("driver") }
|
||||
|
||||
/**
|
||||
* Gets `scrapli.driver.core` package.
|
||||
*/
|
||||
private API::Node scrapliCore() { result = scrapliDriver().getMember("core") }
|
||||
|
||||
/**
|
||||
* A `send_command` method responsible for executing commands on remote secondary servers.
|
||||
*/
|
||||
class ScrapliSendCommand extends RemoteCommandExecution::Range, API::CallNode {
|
||||
ScrapliSendCommand() {
|
||||
this =
|
||||
scrapliCore()
|
||||
.getMember([
|
||||
"AsyncNXOSDriver", "AsyncJunosDriver", "AsyncEOSDriver", "AsyncIOSXEDriver",
|
||||
"AsyncIOSXRDriver", "NXOSDriver", "JunosDriver", "EOSDriver", "IOSXEDriver",
|
||||
"IOSXRDriver"
|
||||
])
|
||||
.getReturn()
|
||||
.getMember("send_command")
|
||||
.getACall()
|
||||
or
|
||||
this = scrapli().getMember("Scrapli").getReturn().getMember("send_command").getACall()
|
||||
or
|
||||
this =
|
||||
scrapliDriver().getMember("GenericDriver").getReturn().getMember("send_command").getACall()
|
||||
}
|
||||
|
||||
override DataFlow::Node getCommand() { result = this.getParameter(0, "command").asSink() }
|
||||
}
|
||||
}
|
||||
39
python/ql/src/experimental/semmle/python/frameworks/Ssh2.qll
Normal file
39
python/ql/src/experimental/semmle/python/frameworks/Ssh2.qll
Normal file
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Provides classes modeling security-relevant aspects of the `ssh2-python` PyPI package.
|
||||
* See https://pypi.org/project/ssh2-python/.
|
||||
*/
|
||||
|
||||
private import python
|
||||
private import semmle.python.dataflow.new.RemoteFlowSources
|
||||
private import semmle.python.Concepts
|
||||
private import semmle.python.ApiGraphs
|
||||
import experimental.semmle.python.Concepts
|
||||
|
||||
/**
|
||||
* Provides models for the `ssh2-python` PyPI package.
|
||||
* See https://pypi.org/project/ssh2-python/.
|
||||
*/
|
||||
private module Ssh2 {
|
||||
/**
|
||||
* Gets `ssh2` package.
|
||||
*/
|
||||
private API::Node ssh2() { result = API::moduleImport("ssh2") }
|
||||
|
||||
/**
|
||||
* Gets `ssh2.session.Session` return value.
|
||||
*/
|
||||
private API::Node ssh2Session() {
|
||||
result = ssh2().getMember("session").getMember("Session").getReturn()
|
||||
}
|
||||
|
||||
/**
|
||||
* An `execute` method responsible for executing commands on remote secondary servers.
|
||||
*/
|
||||
class Ssh2Execute extends RemoteCommandExecution::Range, API::CallNode {
|
||||
Ssh2Execute() {
|
||||
this = ssh2Session().getMember("open_session").getReturn().getMember("execute").getACall()
|
||||
}
|
||||
|
||||
override DataFlow::Node getCommand() { result = this.getParameter(0, "command").asSink() }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Provides classes modeling security-relevant aspects of the `twisted` PyPI package.
|
||||
* See https://twistedmatrix.com/.
|
||||
*/
|
||||
|
||||
private import python
|
||||
private import semmle.python.dataflow.new.RemoteFlowSources
|
||||
private import semmle.python.dataflow.new.TaintTracking
|
||||
private import semmle.python.Concepts
|
||||
private import semmle.python.ApiGraphs
|
||||
private import semmle.python.frameworks.internal.InstanceTaintStepsHelper
|
||||
import experimental.semmle.python.Concepts
|
||||
|
||||
/**
|
||||
* Provides models for the `twisted` PyPI package.
|
||||
* See https://docs.twistedmatrix.com/en/stable/api/twisted.conch.endpoints.SSHCommandClientEndpoint.html
|
||||
*/
|
||||
private module Twisted {
|
||||
/**
|
||||
* The `newConnection` and `existingConnection` functions of `twisted.conch.endpoints.SSHCommandClientEndpoint` class execute command on ssh target server
|
||||
*/
|
||||
class ParamikoExecCommand extends RemoteCommandExecution::Range, API::CallNode {
|
||||
ParamikoExecCommand() {
|
||||
this =
|
||||
API::moduleImport("twisted")
|
||||
.getMember("conch")
|
||||
.getMember("endpoints")
|
||||
.getMember("SSHCommandClientEndpoint")
|
||||
.getMember(["newConnection", "existingConnection"])
|
||||
.getACall()
|
||||
}
|
||||
|
||||
override DataFlow::Node getCommand() { result = this.getParameter(1, "command").asSink() }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import python
|
||||
import semmle.python.dataflow.new.TaintTracking
|
||||
import semmle.python.dataflow.new.RemoteFlowSources
|
||||
import semmle.python.ApiGraphs
|
||||
import semmle.python.dataflow.new.internal.DataFlowPublic
|
||||
import codeql.util.Unit
|
||||
import experimental.semmle.python.Concepts
|
||||
|
||||
module RemoteCommandExecutionConfig implements DataFlow::ConfigSig {
|
||||
predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
|
||||
|
||||
predicate isSink(DataFlow::Node sink) { sink = any(RemoteCommandExecution rce).getCommand() }
|
||||
}
|
||||
|
||||
/** Global taint-tracking for detecting "secondary server command injection" vulnerabilities. */
|
||||
module RemoteCommandExecutionFlow = TaintTracking::Global<RemoteCommandExecutionConfig>;
|
||||
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
from fastapi import FastAPI
|
||||
import asyncssh
|
||||
|
||||
|
||||
app = FastAPI()
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.connect(("host", "port"))
|
||||
session = Session()
|
||||
session.handshake(sock)
|
||||
session.userauth_password("user", "password")
|
||||
|
||||
@app.get("/bad1")
|
||||
async def bad1(cmd: str):
|
||||
async with asyncssh.connect('localhost') as conn:
|
||||
result = await conn.run(cmd, check=True) # $ result=BAD getRemoteCommand=cmd
|
||||
print(result.stdout, end='')
|
||||
return {"success": "Dangerous"}
|
||||
@@ -0,0 +1,2 @@
|
||||
testFailures
|
||||
failures
|
||||
@@ -0,0 +1,23 @@
|
||||
import python
|
||||
import semmle.python.dataflow.new.DataFlow
|
||||
import semmle.python.dataflow.new.internal.DataFlowDispatch as DataFlowDispatch
|
||||
import TestUtilities.InlineExpectationsTest
|
||||
private import semmle.python.dataflow.new.internal.PrintNode
|
||||
import experimental.semmle.python.Concepts
|
||||
|
||||
module SystemCommandExecutionTest implements TestSig {
|
||||
string getARelevantTag() { result = "getRemoteCommand" }
|
||||
|
||||
predicate hasActualResult(Location location, string element, string tag, string value) {
|
||||
exists(location.getFile().getRelativePath()) and
|
||||
exists(RemoteCommandExecution sci, DataFlow::Node command |
|
||||
command = sci.getCommand() and
|
||||
location = command.getLocation() and
|
||||
element = command.toString() and
|
||||
value = prettyNodeForInlineTest(command) and
|
||||
tag = "getRemoteCommand"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
import MakeTest<SystemCommandExecutionTest>
|
||||
@@ -0,0 +1,3 @@
|
||||
missingAnnotationOnSink
|
||||
testFailures
|
||||
failures
|
||||
@@ -0,0 +1,4 @@
|
||||
import python
|
||||
import TestUtilities.dataflow.DataflowQueryTest
|
||||
import experimental.semmle.python.security.RemoteCommandExecution
|
||||
import FromTaintTrackingConfig<RemoteCommandExecutionConfig>
|
||||
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
from fastapi import FastAPI
|
||||
from netmiko import ConnectHandler
|
||||
|
||||
|
||||
app = FastAPI()
|
||||
cisco_881 = {
|
||||
'device_type': 'cisco_ios',
|
||||
'host': '10.10.10.10',
|
||||
'username': 'test',
|
||||
'password': 'password',
|
||||
'port': 8022, # optional, defaults to 22
|
||||
'secret': 'secret', # optional, defaults to ''
|
||||
}
|
||||
|
||||
@app.get("/bad1")
|
||||
async def bad1(cmd: str):
|
||||
net_connect = ConnectHandler(**cisco_881)
|
||||
net_connect.send_command(command_string=cmd) # $ result=BAD getRemoteCommand=cmd
|
||||
net_connect.send_command_expect(command_string=cmd) # $ result=BAD getRemoteCommand=cmd
|
||||
net_connect.send_command_timing(command_string=cmd) # $ result=BAD getRemoteCommand=cmd
|
||||
net_connect.send_multiline(commands=[[cmd, "expect"]]) # $ result=BAD getRemoteCommand=List
|
||||
net_connect.send_multiline_timing(commands=cmd) # $ result=BAD getRemoteCommand=cmd
|
||||
return {"success": "Dangerous"}
|
||||
@@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
from fastapi import FastAPI
|
||||
from pexpect import pxssh
|
||||
|
||||
ssh = pxssh.pxssh()
|
||||
hostname = "localhost"
|
||||
username = "username"
|
||||
password = "password"
|
||||
ssh.login(hostname, username, password)
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
@app.get("/bad1")
|
||||
async def bad1(cmd: str):
|
||||
ssh.send(cmd) # $ result=BAD getRemoteCommand=cmd
|
||||
ssh.prompt()
|
||||
ssh.sendline(cmd) # $ result=BAD getRemoteCommand=cmd
|
||||
ssh.prompt()
|
||||
ssh.logout()
|
||||
return {"success": stdout}
|
||||
@@ -0,0 +1,88 @@
|
||||
edges
|
||||
| AsyncSsh.py:15:16:15:18 | ControlFlowNode for cmd | AsyncSsh.py:17:33:17:35 | ControlFlowNode for cmd | provenance | |
|
||||
| Netmiko.py:18:16:18:18 | ControlFlowNode for cmd | Netmiko.py:20:45:20:47 | ControlFlowNode for cmd | provenance | |
|
||||
| Netmiko.py:18:16:18:18 | ControlFlowNode for cmd | Netmiko.py:21:52:21:54 | ControlFlowNode for cmd | provenance | |
|
||||
| Netmiko.py:18:16:18:18 | ControlFlowNode for cmd | Netmiko.py:22:52:22:54 | ControlFlowNode for cmd | provenance | |
|
||||
| Netmiko.py:18:16:18:18 | ControlFlowNode for cmd | Netmiko.py:23:41:23:57 | ControlFlowNode for List | provenance | |
|
||||
| Netmiko.py:18:16:18:18 | ControlFlowNode for cmd | Netmiko.py:24:48:24:50 | ControlFlowNode for cmd | provenance | |
|
||||
| Pexpect.py:15:16:15:18 | ControlFlowNode for cmd | Pexpect.py:16:14:16:16 | ControlFlowNode for cmd | provenance | |
|
||||
| Pexpect.py:15:16:15:18 | ControlFlowNode for cmd | Pexpect.py:18:18:18:20 | ControlFlowNode for cmd | provenance | |
|
||||
| Scrapli.py:13:16:13:18 | ControlFlowNode for cmd | Scrapli.py:24:42:24:44 | ControlFlowNode for cmd | provenance | |
|
||||
| Scrapli.py:13:16:13:18 | ControlFlowNode for cmd | Scrapli.py:27:42:27:44 | ControlFlowNode for cmd | provenance | |
|
||||
| Scrapli.py:13:16:13:18 | ControlFlowNode for cmd | Scrapli.py:30:42:30:44 | ControlFlowNode for cmd | provenance | |
|
||||
| Scrapli.py:13:16:13:18 | ControlFlowNode for cmd | Scrapli.py:33:42:33:44 | ControlFlowNode for cmd | provenance | |
|
||||
| Scrapli.py:13:16:13:18 | ControlFlowNode for cmd | Scrapli.py:36:42:36:44 | ControlFlowNode for cmd | provenance | |
|
||||
| Scrapli.py:40:10:40:12 | ControlFlowNode for cmd | Scrapli.py:51:36:51:38 | ControlFlowNode for cmd | provenance | |
|
||||
| Scrapli.py:40:10:40:12 | ControlFlowNode for cmd | Scrapli.py:54:36:54:38 | ControlFlowNode for cmd | provenance | |
|
||||
| Scrapli.py:40:10:40:12 | ControlFlowNode for cmd | Scrapli.py:57:36:57:38 | ControlFlowNode for cmd | provenance | |
|
||||
| Scrapli.py:40:10:40:12 | ControlFlowNode for cmd | Scrapli.py:60:36:60:38 | ControlFlowNode for cmd | provenance | |
|
||||
| Scrapli.py:40:10:40:12 | ControlFlowNode for cmd | Scrapli.py:63:36:63:38 | ControlFlowNode for cmd | provenance | |
|
||||
| Scrapli.py:40:10:40:12 | ControlFlowNode for cmd | Scrapli.py:74:36:74:38 | ControlFlowNode for cmd | provenance | |
|
||||
| Scrapli.py:40:10:40:12 | ControlFlowNode for cmd | Scrapli.py:84:36:84:38 | ControlFlowNode for cmd | provenance | |
|
||||
| Twisted.py:13:16:13:18 | ControlFlowNode for cmd | Twisted.py:16:5:16:7 | ControlFlowNode for cmd | provenance | |
|
||||
| Twisted.py:13:16:13:18 | ControlFlowNode for cmd | Twisted.py:24:9:24:11 | ControlFlowNode for cmd | provenance | |
|
||||
| paramiko.py:15:16:15:18 | ControlFlowNode for cmd | paramiko.py:16:62:16:64 | ControlFlowNode for cmd | provenance | |
|
||||
| paramiko.py:20:16:20:18 | ControlFlowNode for cmd | paramiko.py:21:70:21:72 | ControlFlowNode for cmd | provenance | |
|
||||
| ssh2.py:15:16:15:18 | ControlFlowNode for cmd | ssh2.py:17:21:17:23 | ControlFlowNode for cmd | provenance | |
|
||||
nodes
|
||||
| AsyncSsh.py:15:16:15:18 | ControlFlowNode for cmd | semmle.label | ControlFlowNode for cmd |
|
||||
| AsyncSsh.py:17:33:17:35 | ControlFlowNode for cmd | semmle.label | ControlFlowNode for cmd |
|
||||
| Netmiko.py:18:16:18:18 | ControlFlowNode for cmd | semmle.label | ControlFlowNode for cmd |
|
||||
| Netmiko.py:20:45:20:47 | ControlFlowNode for cmd | semmle.label | ControlFlowNode for cmd |
|
||||
| Netmiko.py:21:52:21:54 | ControlFlowNode for cmd | semmle.label | ControlFlowNode for cmd |
|
||||
| Netmiko.py:22:52:22:54 | ControlFlowNode for cmd | semmle.label | ControlFlowNode for cmd |
|
||||
| Netmiko.py:23:41:23:57 | ControlFlowNode for List | semmle.label | ControlFlowNode for List |
|
||||
| Netmiko.py:24:48:24:50 | ControlFlowNode for cmd | semmle.label | ControlFlowNode for cmd |
|
||||
| Pexpect.py:15:16:15:18 | ControlFlowNode for cmd | semmle.label | ControlFlowNode for cmd |
|
||||
| Pexpect.py:16:14:16:16 | ControlFlowNode for cmd | semmle.label | ControlFlowNode for cmd |
|
||||
| Pexpect.py:18:18:18:20 | ControlFlowNode for cmd | semmle.label | ControlFlowNode for cmd |
|
||||
| Scrapli.py:13:16:13:18 | ControlFlowNode for cmd | semmle.label | ControlFlowNode for cmd |
|
||||
| Scrapli.py:24:42:24:44 | ControlFlowNode for cmd | semmle.label | ControlFlowNode for cmd |
|
||||
| Scrapli.py:27:42:27:44 | ControlFlowNode for cmd | semmle.label | ControlFlowNode for cmd |
|
||||
| Scrapli.py:30:42:30:44 | ControlFlowNode for cmd | semmle.label | ControlFlowNode for cmd |
|
||||
| Scrapli.py:33:42:33:44 | ControlFlowNode for cmd | semmle.label | ControlFlowNode for cmd |
|
||||
| Scrapli.py:36:42:36:44 | ControlFlowNode for cmd | semmle.label | ControlFlowNode for cmd |
|
||||
| Scrapli.py:40:10:40:12 | ControlFlowNode for cmd | semmle.label | ControlFlowNode for cmd |
|
||||
| Scrapli.py:51:36:51:38 | ControlFlowNode for cmd | semmle.label | ControlFlowNode for cmd |
|
||||
| Scrapli.py:54:36:54:38 | ControlFlowNode for cmd | semmle.label | ControlFlowNode for cmd |
|
||||
| Scrapli.py:57:36:57:38 | ControlFlowNode for cmd | semmle.label | ControlFlowNode for cmd |
|
||||
| Scrapli.py:60:36:60:38 | ControlFlowNode for cmd | semmle.label | ControlFlowNode for cmd |
|
||||
| Scrapli.py:63:36:63:38 | ControlFlowNode for cmd | semmle.label | ControlFlowNode for cmd |
|
||||
| Scrapli.py:74:36:74:38 | ControlFlowNode for cmd | semmle.label | ControlFlowNode for cmd |
|
||||
| Scrapli.py:84:36:84:38 | ControlFlowNode for cmd | semmle.label | ControlFlowNode for cmd |
|
||||
| Twisted.py:13:16:13:18 | ControlFlowNode for cmd | semmle.label | ControlFlowNode for cmd |
|
||||
| Twisted.py:16:5:16:7 | ControlFlowNode for cmd | semmle.label | ControlFlowNode for cmd |
|
||||
| Twisted.py:24:9:24:11 | ControlFlowNode for cmd | semmle.label | ControlFlowNode for cmd |
|
||||
| paramiko.py:15:16:15:18 | ControlFlowNode for cmd | semmle.label | ControlFlowNode for cmd |
|
||||
| paramiko.py:16:62:16:64 | ControlFlowNode for cmd | semmle.label | ControlFlowNode for cmd |
|
||||
| paramiko.py:20:16:20:18 | ControlFlowNode for cmd | semmle.label | ControlFlowNode for cmd |
|
||||
| paramiko.py:21:70:21:72 | ControlFlowNode for cmd | semmle.label | ControlFlowNode for cmd |
|
||||
| ssh2.py:15:16:15:18 | ControlFlowNode for cmd | semmle.label | ControlFlowNode for cmd |
|
||||
| ssh2.py:17:21:17:23 | ControlFlowNode for cmd | semmle.label | ControlFlowNode for cmd |
|
||||
subpaths
|
||||
#select
|
||||
| AsyncSsh.py:17:33:17:35 | ControlFlowNode for cmd | AsyncSsh.py:15:16:15:18 | ControlFlowNode for cmd | AsyncSsh.py:17:33:17:35 | ControlFlowNode for cmd | This code execution depends on a $@. | AsyncSsh.py:15:16:15:18 | ControlFlowNode for cmd | a user-provided value |
|
||||
| Netmiko.py:20:45:20:47 | ControlFlowNode for cmd | Netmiko.py:18:16:18:18 | ControlFlowNode for cmd | Netmiko.py:20:45:20:47 | ControlFlowNode for cmd | This code execution depends on a $@. | Netmiko.py:18:16:18:18 | ControlFlowNode for cmd | a user-provided value |
|
||||
| Netmiko.py:21:52:21:54 | ControlFlowNode for cmd | Netmiko.py:18:16:18:18 | ControlFlowNode for cmd | Netmiko.py:21:52:21:54 | ControlFlowNode for cmd | This code execution depends on a $@. | Netmiko.py:18:16:18:18 | ControlFlowNode for cmd | a user-provided value |
|
||||
| Netmiko.py:22:52:22:54 | ControlFlowNode for cmd | Netmiko.py:18:16:18:18 | ControlFlowNode for cmd | Netmiko.py:22:52:22:54 | ControlFlowNode for cmd | This code execution depends on a $@. | Netmiko.py:18:16:18:18 | ControlFlowNode for cmd | a user-provided value |
|
||||
| Netmiko.py:23:41:23:57 | ControlFlowNode for List | Netmiko.py:18:16:18:18 | ControlFlowNode for cmd | Netmiko.py:23:41:23:57 | ControlFlowNode for List | This code execution depends on a $@. | Netmiko.py:18:16:18:18 | ControlFlowNode for cmd | a user-provided value |
|
||||
| Netmiko.py:24:48:24:50 | ControlFlowNode for cmd | Netmiko.py:18:16:18:18 | ControlFlowNode for cmd | Netmiko.py:24:48:24:50 | ControlFlowNode for cmd | This code execution depends on a $@. | Netmiko.py:18:16:18:18 | ControlFlowNode for cmd | a user-provided value |
|
||||
| Pexpect.py:16:14:16:16 | ControlFlowNode for cmd | Pexpect.py:15:16:15:18 | ControlFlowNode for cmd | Pexpect.py:16:14:16:16 | ControlFlowNode for cmd | This code execution depends on a $@. | Pexpect.py:15:16:15:18 | ControlFlowNode for cmd | a user-provided value |
|
||||
| Pexpect.py:18:18:18:20 | ControlFlowNode for cmd | Pexpect.py:15:16:15:18 | ControlFlowNode for cmd | Pexpect.py:18:18:18:20 | ControlFlowNode for cmd | This code execution depends on a $@. | Pexpect.py:15:16:15:18 | ControlFlowNode for cmd | a user-provided value |
|
||||
| Scrapli.py:24:42:24:44 | ControlFlowNode for cmd | Scrapli.py:13:16:13:18 | ControlFlowNode for cmd | Scrapli.py:24:42:24:44 | ControlFlowNode for cmd | This code execution depends on a $@. | Scrapli.py:13:16:13:18 | ControlFlowNode for cmd | a user-provided value |
|
||||
| Scrapli.py:27:42:27:44 | ControlFlowNode for cmd | Scrapli.py:13:16:13:18 | ControlFlowNode for cmd | Scrapli.py:27:42:27:44 | ControlFlowNode for cmd | This code execution depends on a $@. | Scrapli.py:13:16:13:18 | ControlFlowNode for cmd | a user-provided value |
|
||||
| Scrapli.py:30:42:30:44 | ControlFlowNode for cmd | Scrapli.py:13:16:13:18 | ControlFlowNode for cmd | Scrapli.py:30:42:30:44 | ControlFlowNode for cmd | This code execution depends on a $@. | Scrapli.py:13:16:13:18 | ControlFlowNode for cmd | a user-provided value |
|
||||
| Scrapli.py:33:42:33:44 | ControlFlowNode for cmd | Scrapli.py:13:16:13:18 | ControlFlowNode for cmd | Scrapli.py:33:42:33:44 | ControlFlowNode for cmd | This code execution depends on a $@. | Scrapli.py:13:16:13:18 | ControlFlowNode for cmd | a user-provided value |
|
||||
| Scrapli.py:36:42:36:44 | ControlFlowNode for cmd | Scrapli.py:13:16:13:18 | ControlFlowNode for cmd | Scrapli.py:36:42:36:44 | ControlFlowNode for cmd | This code execution depends on a $@. | Scrapli.py:13:16:13:18 | ControlFlowNode for cmd | a user-provided value |
|
||||
| Scrapli.py:51:36:51:38 | ControlFlowNode for cmd | Scrapli.py:40:10:40:12 | ControlFlowNode for cmd | Scrapli.py:51:36:51:38 | ControlFlowNode for cmd | This code execution depends on a $@. | Scrapli.py:40:10:40:12 | ControlFlowNode for cmd | a user-provided value |
|
||||
| Scrapli.py:54:36:54:38 | ControlFlowNode for cmd | Scrapli.py:40:10:40:12 | ControlFlowNode for cmd | Scrapli.py:54:36:54:38 | ControlFlowNode for cmd | This code execution depends on a $@. | Scrapli.py:40:10:40:12 | ControlFlowNode for cmd | a user-provided value |
|
||||
| Scrapli.py:57:36:57:38 | ControlFlowNode for cmd | Scrapli.py:40:10:40:12 | ControlFlowNode for cmd | Scrapli.py:57:36:57:38 | ControlFlowNode for cmd | This code execution depends on a $@. | Scrapli.py:40:10:40:12 | ControlFlowNode for cmd | a user-provided value |
|
||||
| Scrapli.py:60:36:60:38 | ControlFlowNode for cmd | Scrapli.py:40:10:40:12 | ControlFlowNode for cmd | Scrapli.py:60:36:60:38 | ControlFlowNode for cmd | This code execution depends on a $@. | Scrapli.py:40:10:40:12 | ControlFlowNode for cmd | a user-provided value |
|
||||
| Scrapli.py:63:36:63:38 | ControlFlowNode for cmd | Scrapli.py:40:10:40:12 | ControlFlowNode for cmd | Scrapli.py:63:36:63:38 | ControlFlowNode for cmd | This code execution depends on a $@. | Scrapli.py:40:10:40:12 | ControlFlowNode for cmd | a user-provided value |
|
||||
| Scrapli.py:74:36:74:38 | ControlFlowNode for cmd | Scrapli.py:40:10:40:12 | ControlFlowNode for cmd | Scrapli.py:74:36:74:38 | ControlFlowNode for cmd | This code execution depends on a $@. | Scrapli.py:40:10:40:12 | ControlFlowNode for cmd | a user-provided value |
|
||||
| Scrapli.py:84:36:84:38 | ControlFlowNode for cmd | Scrapli.py:40:10:40:12 | ControlFlowNode for cmd | Scrapli.py:84:36:84:38 | ControlFlowNode for cmd | This code execution depends on a $@. | Scrapli.py:40:10:40:12 | ControlFlowNode for cmd | a user-provided value |
|
||||
| Twisted.py:16:5:16:7 | ControlFlowNode for cmd | Twisted.py:13:16:13:18 | ControlFlowNode for cmd | Twisted.py:16:5:16:7 | ControlFlowNode for cmd | This code execution depends on a $@. | Twisted.py:13:16:13:18 | ControlFlowNode for cmd | a user-provided value |
|
||||
| Twisted.py:24:9:24:11 | ControlFlowNode for cmd | Twisted.py:13:16:13:18 | ControlFlowNode for cmd | Twisted.py:24:9:24:11 | ControlFlowNode for cmd | This code execution depends on a $@. | Twisted.py:13:16:13:18 | ControlFlowNode for cmd | a user-provided value |
|
||||
| paramiko.py:16:62:16:64 | ControlFlowNode for cmd | paramiko.py:15:16:15:18 | ControlFlowNode for cmd | paramiko.py:16:62:16:64 | ControlFlowNode for cmd | This code execution depends on a $@. | paramiko.py:15:16:15:18 | ControlFlowNode for cmd | a user-provided value |
|
||||
| paramiko.py:21:70:21:72 | ControlFlowNode for cmd | paramiko.py:20:16:20:18 | ControlFlowNode for cmd | paramiko.py:21:70:21:72 | ControlFlowNode for cmd | This code execution depends on a $@. | paramiko.py:20:16:20:18 | ControlFlowNode for cmd | a user-provided value |
|
||||
| ssh2.py:17:21:17:23 | ControlFlowNode for cmd | ssh2.py:15:16:15:18 | ControlFlowNode for cmd | ssh2.py:17:21:17:23 | ControlFlowNode for cmd | This code execution depends on a $@. | ssh2.py:15:16:15:18 | ControlFlowNode for cmd | a user-provided value |
|
||||
@@ -0,0 +1 @@
|
||||
experimental/Security/CWE-074/remoteCommandExecution/RemoteCommandExecution.ql
|
||||
@@ -0,0 +1,85 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
from fastapi import FastAPI
|
||||
from scrapli import Scrapli
|
||||
from scrapli.driver.core import AsyncNXOSDriver, AsyncJunosDriver, AsyncEOSDriver, AsyncIOSXEDriver, AsyncIOSXRDriver
|
||||
from scrapli.driver.core import NXOSDriver, JunosDriver, EOSDriver, IOSXEDriver, IOSXRDriver
|
||||
from scrapli.driver import GenericDriver
|
||||
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
@app.get("/bad1")
|
||||
async def bad1(cmd: str):
|
||||
dev_connect = {
|
||||
"host": host,
|
||||
"auth_username": user,
|
||||
"auth_password": password,
|
||||
"port": port,
|
||||
"auth_strict_key": False,
|
||||
"transport": "asyncssh",
|
||||
}
|
||||
driver = AsyncIOSXEDriver
|
||||
async with driver(**dev_connect) as conn:
|
||||
output = await conn.send_command(cmd) # $ result=BAD getRemoteCommand=cmd
|
||||
driver = AsyncIOSXRDriver
|
||||
async with driver(**dev_connect) as conn:
|
||||
output = await conn.send_command(cmd) # $ result=BAD getRemoteCommand=cmd
|
||||
driver = AsyncNXOSDriver
|
||||
async with driver(**dev_connect) as conn:
|
||||
output = await conn.send_command(cmd) # $ result=BAD getRemoteCommand=cmd
|
||||
driver = AsyncEOSDriver
|
||||
async with driver(**dev_connect) as conn:
|
||||
output = await conn.send_command(cmd) # $ result=BAD getRemoteCommand=cmd
|
||||
driver = AsyncJunosDriver
|
||||
async with driver(**dev_connect) as conn:
|
||||
output = await conn.send_command(cmd) # $ result=BAD getRemoteCommand=cmd
|
||||
return {"success": "Dangerous"}
|
||||
|
||||
@app.get("/bad1")
|
||||
def bad2(cmd: str):
|
||||
dev_connect = {
|
||||
"host": host,
|
||||
"auth_username": user,
|
||||
"auth_password": password,
|
||||
"port": port,
|
||||
"auth_strict_key": False,
|
||||
"transport": "ssh2",
|
||||
}
|
||||
driver = NXOSDriver
|
||||
with driver(**dev_connect) as conn:
|
||||
output = conn.send_command(cmd) # $ result=BAD getRemoteCommand=cmd
|
||||
driver = IOSXRDriver
|
||||
with driver(**dev_connect) as conn:
|
||||
output = conn.send_command(cmd) # $ result=BAD getRemoteCommand=cmd
|
||||
driver = IOSXEDriver
|
||||
with driver(**dev_connect) as conn:
|
||||
output = conn.send_command(cmd) # $ result=BAD getRemoteCommand=cmd
|
||||
driver = EOSDriver
|
||||
with driver(**dev_connect) as conn:
|
||||
output = conn.send_command(cmd) # $ result=BAD getRemoteCommand=cmd
|
||||
driver = JunosDriver
|
||||
with driver(**dev_connect) as conn:
|
||||
output = conn.send_command(cmd) # $ result=BAD getRemoteCommand=cmd
|
||||
|
||||
dev_connect = {
|
||||
"host": "65.65.65.65",
|
||||
"auth_username": "root",
|
||||
"auth_private_key": "keyPath",
|
||||
"auth_strict_key": False,
|
||||
"transport": "ssh2",
|
||||
"platform": "cisco_iosxe",
|
||||
}
|
||||
with Scrapli(**dev_connect) as conn:
|
||||
result = conn.send_command(cmd) # $ result=BAD getRemoteCommand=cmd
|
||||
|
||||
dev_connect = {
|
||||
"host": "65.65.65.65",
|
||||
"auth_username": "root",
|
||||
"auth_password": "password",
|
||||
"auth_strict_key": False,
|
||||
"transport": "ssh2",
|
||||
}
|
||||
with GenericDriver(**dev_connect) as conn:
|
||||
result = conn.send_command(cmd) # $ result=BAD getRemoteCommand=cmd
|
||||
return {"success": "Dangerous"}
|
||||
@@ -0,0 +1,30 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
from fastapi import FastAPI
|
||||
from twisted.conch.endpoints import SSHCommandClientEndpoint
|
||||
from twisted.internet.protocol import Factory
|
||||
from twisted.internet import reactor
|
||||
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.get("/bad1")
|
||||
async def bad1(cmd: bytes):
|
||||
endpoint = SSHCommandClientEndpoint.newConnection(
|
||||
reactor,
|
||||
cmd, # $ result=BAD getRemoteCommand=cmd
|
||||
b"username",
|
||||
b"ssh.example.com",
|
||||
22,
|
||||
password=b"password")
|
||||
|
||||
SSHCommandClientEndpoint.existingConnection(
|
||||
endpoint,
|
||||
cmd) # $ result=BAD getRemoteCommand=cmd
|
||||
|
||||
factory = Factory()
|
||||
d = endpoint.connect(factory)
|
||||
d.addCallback(lambda protocol: protocol.finished)
|
||||
|
||||
return {"success": "Dangerous"}
|
||||
@@ -12,16 +12,11 @@ app = FastAPI()
|
||||
|
||||
|
||||
@app.get("/bad1")
|
||||
async def read_item(cmd: str):
|
||||
stdin, stdout, stderr = paramiko_ssh_client.exec_command(cmd)
|
||||
return {"success": stdout}
|
||||
async def bad1(cmd: str):
|
||||
stdin, stdout, stderr = paramiko_ssh_client.exec_command(cmd) # $ result=BAD getRemoteCommand=cmd
|
||||
return {"success": "Dangerous"}
|
||||
|
||||
@app.get("/bad2")
|
||||
async def read_item(cmd: str):
|
||||
stdin, stdout, stderr = paramiko_ssh_client.exec_command(command=cmd)
|
||||
return {"success": "OK"}
|
||||
|
||||
@app.get("/bad3")
|
||||
async def read_item(cmd: str):
|
||||
stdin, stdout, stderr = paramiko_ssh_client.connect('hostname', username='user',password='yourpassword',sock=paramiko.ProxyCommand(cmd))
|
||||
return {"success": "OK"}
|
||||
async def bad2(cmd: str):
|
||||
stdin, stdout, stderr = paramiko_ssh_client.exec_command(command=cmd) # $ result=BAD getRemoteCommand=cmd
|
||||
return {"success": "Dangerous"}
|
||||
@@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
from fastapi import FastAPI
|
||||
from socket import socket
|
||||
from ssh2.session import Session
|
||||
|
||||
app = FastAPI()
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.connect(("host", "port"))
|
||||
session = Session()
|
||||
session.handshake(sock)
|
||||
session.userauth_password("user", "password")
|
||||
|
||||
@app.get("/bad1")
|
||||
async def bad1(cmd: str):
|
||||
channel = session.open_session()
|
||||
channel.execute(cmd) # $ result=BAD getRemoteCommand=cmd
|
||||
channel.wait_eof()
|
||||
channel.close()
|
||||
channel.wait_closed()
|
||||
return {"success": "Dangerous"}
|
||||
@@ -1 +0,0 @@
|
||||
experimental/Security/CWE-074/paramiko/paramiko.ql
|
||||
@@ -25,3 +25,15 @@ extensions:
|
||||
- ["foo.MS_Class", "Member[instance_method]", "Argument[0]", "ReturnValue.TupleElement[1]", "value"]
|
||||
- ["foo.MS_Class", "Member[explicit_self]", "Argument[self:]", "ReturnValue", "value"]
|
||||
- ["json", "Member[MS_loads]", "Argument[0]", "ReturnValue", "taint"]
|
||||
|
||||
- addsTo:
|
||||
pack: codeql/python-all
|
||||
extensible: typeModel
|
||||
data:
|
||||
- ["foo.MS_Class", "foo", "Member[get_instance].ReturnValue"]
|
||||
- ["foo.MS_Class!", "foo", "Member[get_class].ReturnValue"]
|
||||
# Ideally this would be a consequence of the above line
|
||||
- ["foo.MS_Class", "foo", "Member[get_class].ReturnValue.Instance"]
|
||||
- ["foo.MS_Class", "foo.MS_Factory!", "Member[get_instance].ReturnValue"]
|
||||
- ["foo.MS_Class", "foo.MS_Factory", "Member[make].ReturnValue"]
|
||||
- ["foo.MS_Class", "foo.Impl.MS_Class_Impl", ""]
|
||||
|
||||
@@ -25,3 +25,15 @@ extensions:
|
||||
- ["foo.MS_Class", "Member[instance_method]", "Argument[0]", "ReturnValue.TupleElement[1]", "value"]
|
||||
- ["foo.MS_Class", "Member[explicit_self]", "Argument[self:]", "ReturnValue", "value"]
|
||||
- ["json", "Member[MS_loads]", "Argument[0]", "ReturnValue", "taint"]
|
||||
|
||||
- addsTo:
|
||||
pack: codeql/python-all
|
||||
extensible: typeModel
|
||||
data:
|
||||
- ["foo.MS_Class", "foo", "Member[get_instance].ReturnValue"]
|
||||
- ["foo.MS_Class!", "foo", "Member[get_class].ReturnValue"]
|
||||
# Ideally this would be a consequence of the above line
|
||||
- ["foo.MS_Class", "foo", "Member[get_class].ReturnValue.Instance"]
|
||||
- ["foo.MS_Class", "foo.MS_Factory!", "Member[get_instance].ReturnValue"]
|
||||
- ["foo.MS_Class", "foo.MS_Factory", "Member[make].ReturnValue"]
|
||||
- ["foo.MS_Class", "foo.Impl.MS_Class_Impl", ""]
|
||||
|
||||
@@ -30,7 +30,7 @@ def SINK_F(x):
|
||||
ensure_tainted = ensure_not_tainted = print
|
||||
TAINTED_STRING = "TAINTED_STRING"
|
||||
|
||||
from foo import MS_identity, MS_apply_lambda, MS_reversed, MS_list_map, MS_append_to_list, MS_spread, MS_spread_all
|
||||
from foo import MS_identity, MS_apply_lambda, MS_reversed, MS_list_map, MS_append_to_list, MS_spread, MS_spread_all, Impl
|
||||
|
||||
# Simple summary
|
||||
via_identity = MS_identity(SOURCE)
|
||||
@@ -122,7 +122,7 @@ a, b = MS_spread_all(SOURCE)
|
||||
SINK(a) # $ flow="SOURCE, l:-1 -> a"
|
||||
SINK(b) # $ flow="SOURCE, l:-2 -> b"
|
||||
|
||||
from foo import MS_Class, MS_Class_transitive
|
||||
from foo import MS_Class, MS_Class_transitive, get_instance, get_class, MS_Factory
|
||||
|
||||
# Class summaries
|
||||
class_via_positional = MS_Class(SOURCE)
|
||||
@@ -175,6 +175,24 @@ SINK_F(MS_Class.explicit_self(SOURCE))
|
||||
# Instead, `Argument[self:]` refers to a keyword argument named `self` (which you are allowed to do in Python)
|
||||
SINK(c.explicit_self(self = SOURCE)) # $ flow="SOURCE -> c.explicit_self(..)"
|
||||
|
||||
|
||||
instance = get_instance()
|
||||
SINK(instance.instance_method(SOURCE)[1]) # $ flow="SOURCE -> instance.instance_method(..)[1]"
|
||||
|
||||
returned_class = get_class()
|
||||
SINK(returned_class(SOURCE).config) # $ flow="SOURCE -> returned_class(..).config"
|
||||
|
||||
SINK(returned_class().instance_method(SOURCE)[1]) # $flow="SOURCE -> returned_class().instance_method(..)[1]"
|
||||
|
||||
fatory_instance = MS_Factory.get_instance()
|
||||
SINK(fatory_instance.instance_method(SOURCE)[1]) # $ flow="SOURCE -> fatory_instance.instance_method(..)[1]"
|
||||
|
||||
factory = MS_Factory()
|
||||
SINK(factory.make().instance_method(SOURCE)[1]) # $ flow="SOURCE -> factory.make().instance_method(..)[1]"
|
||||
|
||||
also_instance = Impl.MS_Class_Impl()
|
||||
SINK(also_instance.instance_method(SOURCE)[1]) # $ flow="SOURCE -> also_instance.instance_method(..)[1]"
|
||||
|
||||
# Modeled flow-summary is not value preserving
|
||||
from json import MS_loads as json_loads
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
ERROR: Could not resolve type DataFlow::Add (Test.ql:7,6-19)
|
||||
ERROR: could not resolve type DataFlow::Add (Test.ql:7,6-19)
|
||||
|
||||
@@ -12,3 +12,8 @@ sudo("cmd1; cmd2") # $getCommand="cmd1; cmd2"
|
||||
local(command="cmd1; cmd2") # $getCommand="cmd1; cmd2"
|
||||
run(command="cmd1; cmd2") # $getCommand="cmd1; cmd2"
|
||||
sudo(command="cmd1; cmd2") # $getCommand="cmd1; cmd2"
|
||||
|
||||
from fabric import operations
|
||||
|
||||
operations.local("cmd1; cmd2") # $getCommand="cmd1; cmd2"
|
||||
operations.local(command="cmd1; cmd2") # $getCommand="cmd1; cmd2"
|
||||
|
||||
@@ -5,7 +5,6 @@ Loosely inspired by http://docs.fabfile.org/en/2.5/getting-started.html
|
||||
|
||||
from fabric import connection, Connection, group, SerialGroup, ThreadingGroup, tasks, task
|
||||
|
||||
|
||||
################################################################################
|
||||
# Connection
|
||||
################################################################################
|
||||
@@ -22,6 +21,32 @@ c.sudo(command="cmd1; cmd2") # $getCommand="cmd1; cmd2"
|
||||
c2 = connection.Connection("web2")
|
||||
c2.run("cmd1; cmd2") # $getCommand="cmd1; cmd2"
|
||||
|
||||
# ssh proxy_command command injection with gateway parameter,
|
||||
# we need to call some of the following functions to run the proxy command
|
||||
c = Connection("web1", gateway="cmd") # $getCommand="cmd"
|
||||
c.run(command="cmd1; cmd2") # $getCommand="cmd1; cmd2"
|
||||
c = Connection("web1", gateway="cmd") # $getCommand="cmd"
|
||||
c.get("afs")
|
||||
c = Connection("web1", gateway="cmd") # $getCommand="cmd"
|
||||
c.sudo(command="cmd1; cmd2") # $getCommand="cmd1; cmd2"
|
||||
c = Connection("web1", gateway="cmd") # $getCommand="cmd"
|
||||
c.open_gateway()
|
||||
c = Connection("web1", gateway="cmd") # $getCommand="cmd"
|
||||
c.open()
|
||||
c = Connection("web1", gateway="cmd") # $getCommand="cmd"
|
||||
c.create_session()
|
||||
c = Connection("web1", gateway="cmd") # $getCommand="cmd"
|
||||
c.forward_local("80")
|
||||
c = Connection("web1", gateway="cmd") # $getCommand="cmd"
|
||||
c.forward_remote("80")
|
||||
c = Connection("web1", gateway="cmd") # $getCommand="cmd"
|
||||
c.put(local="local")
|
||||
c = Connection("web1", gateway="cmd") # $getCommand="cmd"
|
||||
c.shell()
|
||||
c = Connection("web1", gateway="cmd") # $getCommand="cmd"
|
||||
c.sftp()
|
||||
# no call to desired methods so it is safe
|
||||
c = Connection("web1", gateway="cmd")
|
||||
|
||||
################################################################################
|
||||
# SerialGroup
|
||||
@@ -30,11 +55,11 @@ results = SerialGroup("web1", "web2", "mac1").run("cmd1; cmd2") # $getCommand="
|
||||
|
||||
pool = SerialGroup("web1", "web2", "web3")
|
||||
pool.run("cmd1; cmd2") # $getCommand="cmd1; cmd2"
|
||||
pool.sudo("cmd1; cmd2") # $getCommand="cmd1; cmd2"
|
||||
|
||||
# fully qualified usage
|
||||
group.SerialGroup("web1", "web2", "mac1").run("cmd1; cmd2") # $getCommand="cmd1; cmd2"
|
||||
|
||||
|
||||
################################################################################
|
||||
# ThreadingGroup
|
||||
################################################################################
|
||||
@@ -42,6 +67,7 @@ results = ThreadingGroup("web1", "web2", "mac1").run("cmd1; cmd2") # $getComman
|
||||
|
||||
pool = ThreadingGroup("web1", "web2", "web3")
|
||||
pool.run("cmd1; cmd2") # $getCommand="cmd1; cmd2"
|
||||
pool.sudo("cmd1; cmd2") # $getCommand="cmd1; cmd2"
|
||||
|
||||
# fully qualified usage
|
||||
group.ThreadingGroup("web1", "web2", "mac1").run("cmd1; cmd2") # $getCommand="cmd1; cmd2"
|
||||
@@ -57,6 +83,7 @@ def foo(c):
|
||||
# 'c' is a fabric.connection.Connection
|
||||
c.run("cmd1; cmd2") # $getCommand="cmd1; cmd2"
|
||||
|
||||
|
||||
# fully qualified usage
|
||||
@tasks.task
|
||||
def bar(c):
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
testFailures
|
||||
failures
|
||||
@@ -0,0 +1,2 @@
|
||||
import python
|
||||
import experimental.meta.ConceptsTest
|
||||
15
python/ql/test/library-tests/frameworks/jsonpickle/Decode.py
Normal file
15
python/ql/test/library-tests/frameworks/jsonpickle/Decode.py
Normal file
@@ -0,0 +1,15 @@
|
||||
import os
|
||||
|
||||
import jsonpickle
|
||||
|
||||
|
||||
class Thing(object):
|
||||
def __reduce__(self):
|
||||
return os.system, ("curl 127.0.0.1:1234",)
|
||||
|
||||
|
||||
obj = Thing()
|
||||
|
||||
pickledObj = jsonpickle.encode(obj)
|
||||
objUnPickled = jsonpickle.decode(pickledObj, safe=True) # $ decodeInput=pickledObj decodeOutput=jsonpickle.decode(..) decodeFormat=pickle decodeMayExecuteInput
|
||||
print(objUnPickled.name)
|
||||
@@ -0,0 +1,85 @@
|
||||
import pandas as pd
|
||||
|
||||
|
||||
df = pd.DataFrame({'temp_c': [17.0, 25.0]}, index=['Portland', 'Berkeley'])
|
||||
df.sample().query("query") # $getCode="query"
|
||||
df.mod().query("query") # $getCode="query"
|
||||
pd.eval("pythonExpr", target=df) # $getCode="pythonExpr"
|
||||
|
||||
df = pd.read_csv("filepath")
|
||||
df.query("query") # $getCode="query"
|
||||
df.eval("query") # $getCode="query"
|
||||
df.copy().query("query") # $getCode="query"
|
||||
|
||||
df = pd.read_fwf("filepath")
|
||||
df.query("query") # $getCode="query"
|
||||
df.eval("query") # $getCode="query"
|
||||
|
||||
|
||||
df = pd.read_pickle("filepath") # $ decodeInput="filepath" decodeOutput=pd.read_pickle(..) decodeFormat=pickle decodeMayExecuteInput
|
||||
df.query("query") # $getCode="query"
|
||||
df.eval("query") # $getCode="query"
|
||||
|
||||
df = pd.read_table("filepath")
|
||||
df.query("query") # $getCode="query"
|
||||
df.eval("query") # $getCode="query"
|
||||
|
||||
df = pd.read_clipboard("filepath")
|
||||
df.query("query") # $getCode="query"
|
||||
df.eval("query") # $getCode="query"
|
||||
|
||||
df = pd.read_excel("filepath")
|
||||
df.query("query") # $getCode="query"
|
||||
df.eval("query") # $getCode="query"
|
||||
|
||||
df = pd.read_html("filepath")
|
||||
df[0].query("query") # $getCode="query"
|
||||
|
||||
df = pd.read_xml("filepath")
|
||||
df.query("query") # $getCode="query"
|
||||
df.eval("query") # $getCode="query"
|
||||
|
||||
df = pd.read_parquet("filepath")
|
||||
df.query("query") # $getCode="query"
|
||||
df.eval("query") # $getCode="query"
|
||||
|
||||
df = pd.read_orc("filepath")
|
||||
df.query("query") # $getCode="query"
|
||||
df.eval("query") # $getCode="query"
|
||||
|
||||
df = pd.read_spss("filepath")
|
||||
df.query("query") # $getCode="query"
|
||||
df.eval("query") # $getCode="query"
|
||||
|
||||
df = pd.read_sql_table("filepath", 'postgres:///db_name')
|
||||
df.query("query") # $getCode="query"
|
||||
df.eval("query") # $getCode="query"
|
||||
|
||||
df = pd.read_sql_query("filepath", 'postgres:///db_name')
|
||||
df.query("query") # $getCode="query"
|
||||
df.eval("query") # $getCode="query"
|
||||
|
||||
df = pd.read_sql("filepath", 'postgres:///db_name')
|
||||
df.query("query") # $getCode="query"
|
||||
df.eval("query") # $getCode="query"
|
||||
|
||||
df = pd.read_gbq("filepath")
|
||||
df.query("query") # $getCode="query"
|
||||
df.eval("query") # $getCode="query"
|
||||
|
||||
df = pd.read_stata("filepath")
|
||||
df.query("query") # $getCode="query"
|
||||
df.eval("query") # $getCode="query"
|
||||
|
||||
df = pd.read_sas("filepath")
|
||||
df.query("query") # $getCode="query"
|
||||
df.eval("query") # $getCode="query"
|
||||
df = pd.read_sas("filepath", iterator=True, chunksize=1)
|
||||
df.query("query")
|
||||
df = pd.read_sas("filepath", iterator=False, chunksize=1)
|
||||
df.query("query")
|
||||
df = pd.read_sas("filepath", iterator=True, chunksize=None)
|
||||
df.query("query")
|
||||
df = pd.read_sas("filepath", iterator=False, chunksize=None)
|
||||
df.query("query") # $getCode="query"
|
||||
df.eval("query") # $getCode="query"
|
||||
@@ -0,0 +1,2 @@
|
||||
testFailures
|
||||
failures
|
||||
@@ -0,0 +1,2 @@
|
||||
import python
|
||||
import experimental.meta.ConceptsTest
|
||||
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import paramiko
|
||||
from paramiko import SSHClient
|
||||
paramiko_ssh_client = SSHClient()
|
||||
paramiko_ssh_client.load_system_host_keys()
|
||||
paramiko_ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
paramiko_ssh_client.connect(hostname="127.0.0.1", port="22", username="ssh_user_name", pkey="k", timeout=11, banner_timeout=200)
|
||||
|
||||
cmd = "cmd"
|
||||
paramiko_ssh_client.connect('hostname', username='user', password='yourpassword', sock=paramiko.ProxyCommand(cmd)) # $getCommand=cmd
|
||||
@@ -0,0 +1,2 @@
|
||||
testFailures
|
||||
failures
|
||||
@@ -0,0 +1,2 @@
|
||||
import python
|
||||
import experimental.meta.ConceptsTest
|
||||
@@ -0,0 +1,9 @@
|
||||
import pexpect
|
||||
from pexpect import popen_spawn
|
||||
|
||||
cmd = "ls -la"
|
||||
result = pexpect.run(cmd) # $ getCommand=cmd
|
||||
result = pexpect.runu(cmd) # $ getCommand=cmd
|
||||
result = pexpect.spawn(cmd) # $ getCommand=cmd
|
||||
result = pexpect.spawnu(cmd) # $ getCommand=cmd
|
||||
result = popen_spawn.PopenSpawn(cmd) # $ getCommand=cmd
|
||||
@@ -0,0 +1,2 @@
|
||||
testFailures
|
||||
failures
|
||||
@@ -0,0 +1,2 @@
|
||||
import python
|
||||
import experimental.meta.ConceptsTest
|
||||
25
python/ql/test/library-tests/frameworks/torch/Decoding.py
Normal file
25
python/ql/test/library-tests/frameworks/torch/Decoding.py
Normal file
@@ -0,0 +1,25 @@
|
||||
from io import BytesIO
|
||||
|
||||
import torch
|
||||
|
||||
|
||||
def someSafeMethod():
|
||||
pass
|
||||
|
||||
|
||||
PicklePayload = BytesIO(b"payload")
|
||||
torch.load(PicklePayload) # $ decodeInput=PicklePayload decodeOutput=torch.load(..) decodeFormat=pickle decodeMayExecuteInput
|
||||
torch.load(PicklePayload, pickle_module=None) # $ decodeInput=PicklePayload decodeOutput=torch.load(..) decodeFormat=pickle decodeMayExecuteInput
|
||||
torch.load(PicklePayload, pickle_module=someSafeMethod()) # $ decodeInput=PicklePayload decodeOutput=torch.load(..) decodeFormat=pickle
|
||||
|
||||
from torch.package import PackageImporter
|
||||
|
||||
importer = PackageImporter(PicklePayload) # $ decodeInput=PicklePayload PackageImporter(..) decodeFormat=pickle decodeMayExecuteInput
|
||||
my_tensor = importer.load_pickle("my_resources", "tensor.pkl") # $ decodeOutput=importer.load_pickle(..)
|
||||
|
||||
importer = PackageImporter(PicklePayload)
|
||||
|
||||
|
||||
from torch import jit
|
||||
|
||||
jit.load(PicklePayload) # $ decodeInput=PicklePayload decodeOutput=jit.load(..) decodeFormat=pickle decodeMayExecuteInput
|
||||
@@ -5,3 +5,4 @@
|
||||
| imports_test.py:10:1:10:22 | Import | Import of 'top_level_cycle' is not used. |
|
||||
| imports_test.py:27:1:27:25 | Import | Import of 'func2' is not used. |
|
||||
| imports_test.py:34:1:34:14 | Import | Import of 'module2' is not used. |
|
||||
| imports_test.py:116:1:116:41 | Import | Import of 'not_a_fixture' is not used. |
|
||||
|
||||
@@ -111,3 +111,8 @@ import subexpression_return_type
|
||||
|
||||
def baz() -> Optional['subexpression_return_type']:
|
||||
pass
|
||||
|
||||
|
||||
from pytest_fixtures import not_a_fixture # BAD
|
||||
from pytest_fixtures import fixture, wrapped_fixture # GOOD (pytest fixtures are used implicitly by pytest)
|
||||
from pytest_fixtures import session_fixture, wrapped_autouse_fixture # GOOD (pytest fixtures are used implicitly by pytest)
|
||||
|
||||
34
python/ql/test/query-tests/Imports/unused/pytest_fixtures.py
Normal file
34
python/ql/test/query-tests/Imports/unused/pytest_fixtures.py
Normal file
@@ -0,0 +1,34 @@
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fixture():
|
||||
pass
|
||||
|
||||
def fixture_wrapper():
|
||||
@pytest.fixture
|
||||
def delegate():
|
||||
pass
|
||||
return delegate
|
||||
|
||||
@fixture_wrapper
|
||||
def wrapped_fixture():
|
||||
pass
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def session_fixture():
|
||||
pass
|
||||
|
||||
def not_a_fixture():
|
||||
pass
|
||||
|
||||
def another_fixture_wrapper():
|
||||
@pytest.fixture(autouse=True)
|
||||
def delegate():
|
||||
pass
|
||||
return delegate
|
||||
|
||||
@another_fixture_wrapper
|
||||
def wrapped_autouse_fixture():
|
||||
pass
|
||||
@@ -1,7 +1,7 @@
|
||||
from flask import request
|
||||
|
||||
import requests
|
||||
|
||||
import re
|
||||
|
||||
def full_ssrf():
|
||||
user_input = request.args['untrusted_input']
|
||||
@@ -120,3 +120,57 @@ def partial_ssrf_6():
|
||||
|
||||
url = f"https://example.com/foo#{user_input}"
|
||||
requests.get(url) # NOT OK -- user only controlled fragment
|
||||
|
||||
def partial_ssrf_7():
|
||||
user_input = request.args['untrusted_input']
|
||||
|
||||
if user_input.isalnum():
|
||||
url = f"https://example.com/foo#{user_input}"
|
||||
requests.get(url) # OK - user input can only contain alphanumerical characters
|
||||
|
||||
if user_input.isalpha():
|
||||
url = f"https://example.com/foo#{user_input}"
|
||||
requests.get(url) # OK - user input can only contain alphabetical characters
|
||||
|
||||
if user_input.isdecimal():
|
||||
url = f"https://example.com/foo#{user_input}"
|
||||
requests.get(url) # OK - user input can only contain decimal characters
|
||||
|
||||
if user_input.isdigit():
|
||||
url = f"https://example.com/foo#{user_input}"
|
||||
requests.get(url) # OK - user input can only contain digits
|
||||
|
||||
if user_input.isnumeric():
|
||||
url = f"https://example.com/foo#{user_input}"
|
||||
requests.get(url) # OK - user input can only contain numeric characters
|
||||
|
||||
if user_input.isspace():
|
||||
url = f"https://example.com/foo#{user_input}"
|
||||
requests.get(url) # OK - user input can only contain whitespace characters
|
||||
|
||||
if re.fullmatch(r'[a-zA-Z0-9]+', user_input):
|
||||
url = f"https://example.com/foo#{user_input}"
|
||||
requests.get(url) # OK - user input can only contain alphanumerical characters
|
||||
|
||||
if re.fullmatch(r'.*[a-zA-Z0-9]+.*', user_input):
|
||||
url = f"https://example.com/foo#{user_input}"
|
||||
requests.get(url) # NOT OK, but NOT FOUND - user input can contain arbitrary characters
|
||||
|
||||
|
||||
if re.match(r'^[a-zA-Z0-9]+$', user_input):
|
||||
url = f"https://example.com/foo#{user_input}"
|
||||
requests.get(url) # OK - user input can only contain alphanumerical characters
|
||||
|
||||
if re.match(r'[a-zA-Z0-9]+', user_input):
|
||||
url = f"https://example.com/foo#{user_input}"
|
||||
requests.get(url) # NOT OK, but NOT FOUND - user input can contain arbitrary character as a suffix.
|
||||
|
||||
reg = re.compile(r'^[a-zA-Z0-9]+$')
|
||||
|
||||
if reg.match(user_input):
|
||||
url = f"https://example.com/foo#{user_input}"
|
||||
requests.get(url) # OK - user input can only contain alphanumerical characters
|
||||
|
||||
if reg.fullmatch(user_input):
|
||||
url = f"https://example.com/foo#{user_input}"
|
||||
requests.get(url) # OK - user input can only contain alphanumerical characters
|
||||
|
||||
Reference in New Issue
Block a user