Skip to content

Commit 4a448f4

Browse files
authored
Merge pull request #15715 from am0o0/am0o0-python-codeExec
Python: New command execution sinks
2 parents 49f74ba + eb1999f commit 4a448f4

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+1272
-153
lines changed

python/ql/lib/semmle/python/Frameworks.qll

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ private import semmle.python.frameworks.Idna
3535
private import semmle.python.frameworks.Invoke
3636
private import semmle.python.frameworks.Jmespath
3737
private import semmle.python.frameworks.Joblib
38+
private import semmle.python.frameworks.JsonPickle
3839
private import semmle.python.frameworks.Ldap
3940
private import semmle.python.frameworks.Ldap3
4041
private import semmle.python.frameworks.Libtaxii
@@ -48,7 +49,9 @@ private import semmle.python.frameworks.Numpy
4849
private import semmle.python.frameworks.Opml
4950
private import semmle.python.frameworks.Oracledb
5051
private import semmle.python.frameworks.Pandas
52+
private import semmle.python.frameworks.Paramiko
5153
private import semmle.python.frameworks.Peewee
54+
private import semmle.python.frameworks.Pexpect
5255
private import semmle.python.frameworks.Phoenixdb
5356
private import semmle.python.frameworks.Psycopg
5457
private import semmle.python.frameworks.Psycopg2
@@ -71,6 +74,7 @@ private import semmle.python.frameworks.SqlAlchemy
7174
private import semmle.python.frameworks.Starlette
7275
private import semmle.python.frameworks.Stdlib
7376
private import semmle.python.frameworks.Toml
77+
private import semmle.python.frameworks.Torch
7478
private import semmle.python.frameworks.Tornado
7579
private import semmle.python.frameworks.Twisted
7680
private import semmle.python.frameworks.Ujson

python/ql/lib/semmle/python/frameworks/Fabric.qll

Lines changed: 44 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ private module FabricV1 {
7171
* Provides classes modeling security-relevant aspects of the `fabric` PyPI package, for
7272
* version 2.x.
7373
*
74-
* See http://docs.fabfile.org/en/2.5/getting-st arted.html.
74+
* See http://docs.fabfile.org/en/2.5/getting-started.html.
7575
*/
7676
module FabricV2 {
7777
/** Gets a reference to the `fabric` module. */
@@ -93,7 +93,9 @@ module FabricV2 {
9393
* See https://docs.fabfile.org/en/2.5/api/connection.html#fabric.connection.Connection.
9494
*/
9595
module ConnectionClass {
96-
/** Gets a reference to the `fabric.connection.Connection` class. */
96+
/**
97+
* Gets a reference to the `fabric.connection.Connection` class.
98+
*/
9799
API::Node classRef() {
98100
result = fabric().getMember("Connection")
99101
or
@@ -109,40 +111,16 @@ module FabricV2 {
109111
* This can include instantiations of the class, return values from function
110112
* calls, or a special parameter that will be set when functions are called by an external
111113
* library.
112-
*
113-
* Use the predicate `Connection::instance()` to get references to instances of `fabric.connection.Connection`.
114114
*/
115-
abstract class InstanceSource extends DataFlow::LocalSourceNode { }
116-
117-
private class ClassInstantiation extends InstanceSource, DataFlow::CallCfgNode {
118-
ClassInstantiation() { this = classRef().getACall() }
119-
}
120-
121-
/** Gets a reference to an instance of `fabric.connection.Connection`. */
122-
private DataFlow::TypeTrackingNode instance(DataFlow::TypeTracker t) {
123-
t.start() and
124-
result instanceof InstanceSource
125-
or
126-
exists(DataFlow::TypeTracker t2 | result = instance(t2).track(t2, t))
115+
abstract class Instance extends API::Node {
116+
override string toString() { result = this.(API::Node).toString() }
127117
}
128118

129-
/** Gets a reference to an instance of `fabric.connection.Connection`. */
130-
DataFlow::Node instance() { instance(DataFlow::TypeTracker::end()).flowsTo(result) }
131-
132119
/**
133-
* Gets a reference to either `run`, `sudo`, or `local` method on a
134-
* `fabric.connection.Connection` instance.
135-
*
136-
* See
137-
* - https://docs.fabfile.org/en/2.5/api/connection.html#fabric.connection.Connection.run
138-
* - https://docs.fabfile.org/en/2.5/api/connection.html#fabric.connection.Connection.sudo
139-
* - https://docs.fabfile.org/en/2.5/api/connection.html#fabric.connection.Connection.local
120+
* A reference to the `fabric.connection.Connection` class.
140121
*/
141-
private DataFlow::TypeTrackingNode instanceRunMethods(DataFlow::TypeTracker t) {
142-
t.startInAttr(["run", "sudo", "local"]) and
143-
result = instance()
144-
or
145-
exists(DataFlow::TypeTracker t2 | result = instanceRunMethods(t2).track(t2, t))
122+
class ClassInstantiation extends Instance {
123+
ClassInstantiation() { this = classRef().getReturn() }
146124
}
147125

148126
/**
@@ -154,8 +132,8 @@ module FabricV2 {
154132
* - https://docs.fabfile.org/en/2.5/api/connection.html#fabric.connection.Connection.sudo
155133
* - https://docs.fabfile.org/en/2.5/api/connection.html#fabric.connection.Connection.local
156134
*/
157-
DataFlow::Node instanceRunMethods() {
158-
instanceRunMethods(DataFlow::TypeTracker::end()).flowsTo(result)
135+
API::CallNode instanceRunMethods() {
136+
result = any(Instance is).getMember(["run", "sudo", "local"]).getACall()
159137
}
160138
}
161139
}
@@ -168,19 +146,38 @@ module FabricV2 {
168146
* - https://docs.fabfile.org/en/2.5/api/connection.html#fabric.connection.Connection.local
169147
*/
170148
private class FabricConnectionRunSudoLocalCall extends SystemCommandExecution::Range,
171-
DataFlow::CallCfgNode
149+
API::CallNode
172150
{
173151
FabricConnectionRunSudoLocalCall() {
174-
this.getFunction() = Fabric::Connection::ConnectionClass::instanceRunMethods()
152+
this = Fabric::Connection::ConnectionClass::instanceRunMethods()
175153
}
176154

177-
override DataFlow::Node getCommand() {
178-
result = [this.getArg(0), this.getArgByName("command")]
179-
}
155+
override DataFlow::Node getCommand() { result = this.getParameter(0, "command").asSink() }
180156

181157
override predicate isShellInterpreted(DataFlow::Node arg) { arg = this.getCommand() }
182158
}
183159

160+
/**
161+
* A `gateway` parameter of `fabric.connection.Connection` instance is considered as ssh proxy_command option and can execute command.
162+
* See https://docs.fabfile.org/en/latest/api/connection.html#fabric.connection.Connection
163+
*/
164+
private class FabricConnectionProxyCommand extends SystemCommandExecution::Range, API::CallNode {
165+
FabricConnectionProxyCommand() {
166+
this = Fabric::Connection::ConnectionClass::classRef().getACall() and
167+
// we want to make sure that the connection is established otherwise the command of proxy_command won't run.
168+
exists(
169+
this.getAMethodCall([
170+
"run", "get", "sudo", "open_gateway", "open", "create_session", "forward_local",
171+
"forward_remote", "put", "shell", "sftp"
172+
])
173+
)
174+
}
175+
176+
override DataFlow::Node getCommand() { result = this.getParameter(4, "gateway").asSink() }
177+
178+
override predicate isShellInterpreted(DataFlow::Node arg) { none() }
179+
}
180+
184181
// -------------------------------------------------------------------------
185182
// fabric.tasks
186183
// -------------------------------------------------------------------------
@@ -193,14 +190,10 @@ module FabricV2 {
193190
API::Node task() { result in [tasks().getMember("task"), fabric().getMember("task")] }
194191
}
195192

196-
class FabricTaskFirstParamConnectionInstance extends Fabric::Connection::ConnectionClass::InstanceSource,
197-
DataFlow::ParameterNode
193+
class FabricTaskFirstParamConnectionInstance extends Fabric::Connection::ConnectionClass::Instance
198194
{
199195
FabricTaskFirstParamConnectionInstance() {
200-
exists(Function func |
201-
func.getADecorator() = Fabric::Tasks::task().getAValueReachableFromSource().asExpr() and
202-
this.getParameter() = func.getArg(0)
203-
)
196+
this = Fabric::Tasks::task().getParameter(0).getParameter(0)
204197
}
205198
}
206199

@@ -242,26 +235,27 @@ module FabricV2 {
242235
API::Node subclassInstance() { result = any(ModeledSubclass m).getASubclass*().getReturn() }
243236

244237
/**
245-
* Gets a reference to the `run` method on an instance of a subclass of `fabric.group.Group`.
238+
* Gets a reference to the `run` and `sudo` methods on an instance of a subclass of `fabric.group.Group`.
246239
*
247240
* See https://docs.fabfile.org/en/2.5/api/group.html#fabric.group.Group.run
241+
* See https://docs.fabfile.org/en/latest/api/group.html#fabric.group.Group.sudo
248242
*/
249-
API::Node subclassInstanceRunMethod() { result = subclassInstance().getMember("run") }
243+
API::Node subclassInstanceRunMethod() {
244+
result = subclassInstance().getMember(["run", "sudo"])
245+
}
250246
}
251247

252248
/**
253249
* A call to `run` on an instance of a subclass of `fabric.group.Group`.
254250
*
255251
* See https://docs.fabfile.org/en/2.5/api/group.html#fabric.group.Group.run
256252
*/
257-
private class FabricGroupRunCall extends SystemCommandExecution::Range, DataFlow::CallCfgNode {
253+
private class FabricGroupRunCall extends SystemCommandExecution::Range, API::CallNode {
258254
FabricGroupRunCall() {
259255
this = Fabric::Group::GroupClass::subclassInstanceRunMethod().getACall()
260256
}
261257

262-
override DataFlow::Node getCommand() {
263-
result = [this.getArg(0), this.getArgByName("command")]
264-
}
258+
override DataFlow::Node getCommand() { result = this.getParameter(0, "command").asSink() }
265259

266260
override predicate isShellInterpreted(DataFlow::Node arg) { arg = this.getCommand() }
267261
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/**
2+
* Provides classes modeling security-relevant aspects of the `jsonpickle` PyPI package.
3+
* See https://pypi.org/project/jsonpickle/.
4+
*/
5+
6+
private import python
7+
private import semmle.python.dataflow.new.RemoteFlowSources
8+
private import semmle.python.Concepts
9+
private import semmle.python.ApiGraphs
10+
11+
/**
12+
* Provides models for the `jsonpickle` PyPI package.
13+
* See https://pypi.org/project/jsonpickle/.
14+
*/
15+
private module Jsonpickle {
16+
/**
17+
* A Call to `jsonpickle.decode`.
18+
* See https://jsonpickle.readthedocs.io/en/latest/api.html#jsonpickle.decode
19+
*/
20+
private class JsonpickleDecode extends Decoding::Range, API::CallNode {
21+
JsonpickleDecode() { this = API::moduleImport("jsonpickle").getMember("decode").getACall() }
22+
23+
override predicate mayExecuteInput() { any() }
24+
25+
override DataFlow::Node getAnInput() { result = this.getParameter(0, "string").asSink() }
26+
27+
override DataFlow::Node getOutput() { result = this }
28+
29+
override string getFormat() { result = "pickle" }
30+
}
31+
}

python/ql/lib/semmle/python/frameworks/Pandas.qll

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,121 @@ private module Pandas {
3434

3535
override string getFormat() { result = "pickle" }
3636
}
37+
38+
/**
39+
* Provides security related models for `pandas.DataFrame`.
40+
* See https://pandas.pydata.org/docs/reference/frame.html
41+
*/
42+
module DataFrame {
43+
/**
44+
* A `pandas.DataFrame` Object.
45+
*
46+
* Extend this class to model new APIs.
47+
* See https://pandas.pydata.org/docs/reference/frame.html
48+
*/
49+
abstract class DataFrame extends API::Node {
50+
override string toString() { result = this.(API::Node).toString() }
51+
}
52+
53+
/**
54+
* A `pandas.DataFrame` instantiation.
55+
* See https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.html
56+
*/
57+
class DataFrameConstructor extends DataFrame {
58+
DataFrameConstructor() {
59+
this = API::moduleImport("pandas").getMember("DataFrame").getReturn()
60+
}
61+
}
62+
63+
/**
64+
* The `pandas.read_*` functions that return a `pandas.DataFrame`.
65+
* See https://pandas.pydata.org/docs/reference/io.html
66+
*/
67+
class InputRead extends DataFrame {
68+
InputRead() {
69+
this =
70+
API::moduleImport("pandas")
71+
.getMember([
72+
"read_csv", "read_fwf", "read_pickle", "read_table", "read_clipboard",
73+
"read_excel", "read_xml", "read_parquet", "read_orc", "read_spss",
74+
"read_sql_table", "read_sql_query", "read_sql", "read_gbq", "read_stata"
75+
])
76+
.getReturn()
77+
or
78+
this = API::moduleImport("pandas").getMember("read_html").getReturn().getASubscript()
79+
or
80+
exists(API::Node readSas, API::CallNode readSasCall |
81+
readSas = API::moduleImport("pandas").getMember("read_sas") and
82+
this = readSas.getReturn() and
83+
readSasCall = readSas.getACall()
84+
|
85+
// Returns DataFrame if iterator=False and chunksize=None, Also with default values it returns DataFrame.
86+
(
87+
not readSasCall.getParameter(5, "iterator").asSink().asExpr().(BooleanLiteral)
88+
instanceof True
89+
or
90+
not exists(readSasCall.getParameter(5, "iterator").asSink())
91+
) and
92+
not exists(
93+
readSasCall.getParameter(4, "chunksize").asSink().asExpr().(IntegerLiteral).getN()
94+
)
95+
)
96+
}
97+
}
98+
99+
/**
100+
* The `pandas.DataFrame.*` methods that return a `pandas.DataFrame` object.
101+
* See https://pandas.pydata.org/docs/reference/io.html
102+
*/
103+
class DataFrameMethods extends DataFrame {
104+
DataFrameMethods() {
105+
this =
106+
any(DataFrame df)
107+
.getMember([
108+
"copy", "from_records", "from_dict", "from_spmatrix", "assign", "select_dtypes",
109+
"set_flags", "astype", "infer_objects", "head", "xs", "get", "isin", "where",
110+
"mask", "query", "add", "mul", "truediv", "mod", "pow", "dot", "radd", "rsub",
111+
"rdiv", "rfloordiv", "rtruediv", "rpow", "lt", "gt", "le", "ne", "agg", "combine",
112+
"apply", "aggregate", "transform", "all", "any", "clip", "corr", "cov", "cummax",
113+
"cummin", "cumprod", "describe", "mode", "pct_change", "quantile", "rank",
114+
"round", "sem", "add_prefix", "add_suffix", "at_time", "between_time", "drop",
115+
"drop_duplicates", "filter", "first", "head", "idxmin", "last", "reindex",
116+
"reindex_like", "reset_index", "sample", "set_axis", "tail", "take", "truncate",
117+
"bfill", "dropna", "ffill", "fillna", "interpolate", "isna", "isnull", "notna",
118+
"notnull", "pad", "replace", "droplevel", "pivot", "pivot_table",
119+
"reorder_levels", "sort_values", "sort_index", "nlargest", "nsmallest",
120+
"swaplevel", "stack", "unstack", "isnull", "notna", "notnull", "replace",
121+
"droplevel", "pivot", "pivot_table", "reorder_levels", "sort_values",
122+
"sort_index", "nlargest", "nsmallest", "swaplevel", "stack", "unstack", "melt",
123+
"explode", "squeeze", "T", "transpose", "compare", "join", "from_spmatrix",
124+
"shift", "asof", "merge", "from_dict", "tz_convert", "to_period", "asfreq",
125+
"to_dense", "tz_localize", "box", "__dataframe__"
126+
])
127+
.getReturn()
128+
}
129+
}
130+
}
131+
132+
/**
133+
* A Call to `pandas.DataFrame.query` or `pandas.DataFrame.eval`.
134+
* See https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.query.html
135+
* https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.eval.html
136+
*/
137+
class CodeExecutionCall extends CodeExecution::Range, API::CallNode {
138+
CodeExecutionCall() {
139+
this = any(DataFrame::DataFrame df).getMember(["query", "eval"]).getACall()
140+
}
141+
142+
override DataFlow::Node getCode() { result = this.getParameter(0, "expr").asSink() }
143+
}
144+
145+
/**
146+
* A Call to `pandas.eval`.
147+
* See https://pandas.pydata.org/docs/reference/api/pandas.eval.html
148+
*/
149+
class PandasEval extends CodeExecution::Range, API::CallNode {
150+
PandasEval() { this = API::moduleImport("pandas").getMember("eval").getACall() }
151+
152+
override DataFlow::Node getCode() { result = this.getParameter(0, "expr").asSink() }
153+
}
37154
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/**
2+
* Provides classes modeling security-relevant aspects of the `paramiko` PyPI package.
3+
* See https://pypi.org/project/paramiko/.
4+
*/
5+
6+
private import python
7+
private import semmle.python.dataflow.new.RemoteFlowSources
8+
private import semmle.python.Concepts
9+
private import semmle.python.ApiGraphs
10+
11+
/**
12+
* Provides models for the `paramiko` PyPI package.
13+
* See https://pypi.org/project/paramiko/.
14+
*/
15+
private module Paramiko {
16+
/**
17+
* The first argument of `paramiko.ProxyCommand`.
18+
*
19+
* the `paramiko.ProxyCommand` is equivalent of `ssh -o ProxyCommand="CMD"`
20+
* which runs `CMD` on the local system.
21+
* See https://paramiko.pydata.org/docs/reference/api/paramiko.eval.html
22+
*/
23+
class ParamikoProxyCommand extends SystemCommandExecution::Range, API::CallNode {
24+
ParamikoProxyCommand() {
25+
this = API::moduleImport("paramiko").getMember("ProxyCommand").getACall()
26+
}
27+
28+
override DataFlow::Node getCommand() { result = this.getParameter(0, "command_line").asSink() }
29+
30+
override predicate isShellInterpreted(DataFlow::Node arg) { none() }
31+
}
32+
}

0 commit comments

Comments
 (0)