Skip to content

Commit d234a53

Browse files
committed
update Fabric models, add new sink to Fabric, add proper test cases
1 parent 076faa3 commit d234a53

File tree

2 files changed

+77
-57
lines changed

2 files changed

+77
-57
lines changed

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

Lines changed: 48 additions & 55 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,56 +93,26 @@ 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. */
97-
API::Node classRef() {
98-
result = fabric().getMember("Connection")
99-
or
100-
result = connection().getMember("Connection")
101-
or
102-
result =
103-
ModelOutput::getATypeNode("fabric.connection.Connection~Subclass").getASubclass*()
104-
}
105-
10696
/**
10797
* A source of instances of `fabric.connection.Connection`, extend this class to model new instances.
10898
*
10999
* This can include instantiations of the class, return values from function
110100
* calls, or a special parameter that will be set when functions are called by an external
111101
* library.
112-
*
113-
* Use the predicate `Connection::instance()` to get references to instances of `fabric.connection.Connection`.
114102
*/
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))
127-
}
128-
129-
/** Gets a reference to an instance of `fabric.connection.Connection`. */
130-
DataFlow::Node instance() { instance(DataFlow::TypeTracker::end()).flowsTo(result) }
103+
abstract class Instance extends DataFlow::LocalSourceNode { }
131104

132105
/**
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
106+
* A reference to the `fabric.connection.Connection` class.
140107
*/
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))
108+
class ClassInstantiation extends Instance {
109+
ClassInstantiation() {
110+
this =
111+
[
112+
fabric().getMember("Connection"), connection().getMember("Connection"),
113+
ModelOutput::getATypeNode("fabric.connection.Connection~Subclass").getASubclass*()
114+
].getACall()
115+
}
146116
}
147117

148118
/**
@@ -154,8 +124,8 @@ module FabricV2 {
154124
* - https://docs.fabfile.org/en/2.5/api/connection.html#fabric.connection.Connection.sudo
155125
* - https://docs.fabfile.org/en/2.5/api/connection.html#fabric.connection.Connection.local
156126
*/
157-
DataFlow::Node instanceRunMethods() {
158-
instanceRunMethods(DataFlow::TypeTracker::end()).flowsTo(result)
127+
API::CallNode instanceRunMethods() {
128+
result = any(Instance is).getAMethodCall(["run", "sudo", "local"])
159129
}
160130
}
161131
}
@@ -168,19 +138,38 @@ module FabricV2 {
168138
* - https://docs.fabfile.org/en/2.5/api/connection.html#fabric.connection.Connection.local
169139
*/
170140
private class FabricConnectionRunSudoLocalCall extends SystemCommandExecution::Range,
171-
DataFlow::CallCfgNode
141+
API::CallNode
172142
{
173143
FabricConnectionRunSudoLocalCall() {
174-
this.getFunction() = Fabric::Connection::ConnectionClass::instanceRunMethods()
144+
this = Fabric::Connection::ConnectionClass::instanceRunMethods()
175145
}
176146

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

181149
override predicate isShellInterpreted(DataFlow::Node arg) { arg = this.getCommand() }
182150
}
183151

152+
/**
153+
* A `gateway` parameter of `fabric.connection.Connection` instance is considered as ssh proxy_command option and can execute command.
154+
* See https://docs.fabfile.org/en/latest/api/connection.html#fabric.connection.Connection
155+
*/
156+
private class FabricConnectionProxyCommand extends SystemCommandExecution::Range, API::CallNode {
157+
FabricConnectionProxyCommand() {
158+
this instanceof Fabric::Connection::ConnectionClass::Instance and
159+
// we want to make sure that the connection is established otherwise the command of proxy_command won't run.
160+
exists(
161+
this.getAMethodCall([
162+
"run", "get", "sudo", "open_gateway", "open", "create_session", "forward_local",
163+
"forward_remote", "put", "shell", "sftp"
164+
])
165+
)
166+
}
167+
168+
override DataFlow::Node getCommand() { result = this.getParameter(4, "gateway").asSink() }
169+
170+
override predicate isShellInterpreted(DataFlow::Node arg) { none() }
171+
}
172+
184173
// -------------------------------------------------------------------------
185174
// fabric.tasks
186175
// -------------------------------------------------------------------------
@@ -191,9 +180,12 @@ module FabricV2 {
191180
module Tasks {
192181
/** Gets a reference to the `fabric.tasks.task` decorator. */
193182
API::Node task() { result in [tasks().getMember("task"), fabric().getMember("task")] }
183+
184+
/** Gets a reference to the `fabric.tasks.task` decorator. */
185+
API::Node test() { result in [tasks().getMember("task"), fabric().getMember("task")] }
194186
}
195187

196-
class FabricTaskFirstParamConnectionInstance extends Fabric::Connection::ConnectionClass::InstanceSource,
188+
class FabricTaskFirstParamConnectionInstance extends Fabric::Connection::ConnectionClass::Instance,
197189
DataFlow::ParameterNode
198190
{
199191
FabricTaskFirstParamConnectionInstance() {
@@ -242,26 +234,27 @@ module FabricV2 {
242234
API::Node subclassInstance() { result = any(ModeledSubclass m).getASubclass*().getReturn() }
243235

244236
/**
245-
* Gets a reference to the `run` method on an instance of a subclass of `fabric.group.Group`.
237+
* Gets a reference to the `run` and `sudo` methods on an instance of a subclass of `fabric.group.Group`.
246238
*
247239
* See https://docs.fabfile.org/en/2.5/api/group.html#fabric.group.Group.run
240+
* See https://docs.fabfile.org/en/latest/api/group.html#fabric.group.Group.sudo
248241
*/
249-
API::Node subclassInstanceRunMethod() { result = subclassInstance().getMember("run") }
242+
API::Node subclassInstanceRunMethod() {
243+
result = subclassInstance().getMember(["run", "sudo"])
244+
}
250245
}
251246

252247
/**
253248
* A call to `run` on an instance of a subclass of `fabric.group.Group`.
254249
*
255250
* See https://docs.fabfile.org/en/2.5/api/group.html#fabric.group.Group.run
256251
*/
257-
private class FabricGroupRunCall extends SystemCommandExecution::Range, DataFlow::CallCfgNode {
252+
private class FabricGroupRunCall extends SystemCommandExecution::Range, API::CallNode {
258253
FabricGroupRunCall() {
259254
this = Fabric::Group::GroupClass::subclassInstanceRunMethod().getACall()
260255
}
261256

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

266259
override predicate isShellInterpreted(DataFlow::Node arg) { arg = this.getCommand() }
267260
}

python/ql/test/library-tests/frameworks/fabric/fabric_v2_test.py

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55

66
from fabric import connection, Connection, group, SerialGroup, ThreadingGroup, tasks, task
77

8-
98
################################################################################
109
# Connection
1110
################################################################################
@@ -22,6 +21,32 @@
2221
c2 = connection.Connection("web2")
2322
c2.run("cmd1; cmd2") # $getCommand="cmd1; cmd2"
2423

24+
# ssh proxy_command command injection with gateway parameter,
25+
# we need to call some of the following functions to run the proxy command
26+
c = Connection("web1", gateway="cmd") # $getCommand="cmd"
27+
c.run(command="cmd1; cmd2") # $getCommand="cmd1; cmd2"
28+
c = Connection("web1", gateway="cmd") # $getCommand="cmd"
29+
c.get("afs")
30+
c = Connection("web1", gateway="cmd") # $getCommand="cmd"
31+
c.sudo(command="cmd1; cmd2") # $getCommand="cmd1; cmd2"
32+
c = Connection("web1", gateway="cmd") # $getCommand="cmd"
33+
c.open_gateway()
34+
c = Connection("web1", gateway="cmd") # $getCommand="cmd"
35+
c.open()
36+
c = Connection("web1", gateway="cmd") # $getCommand="cmd"
37+
c.create_session()
38+
c = Connection("web1", gateway="cmd") # $getCommand="cmd"
39+
c.forward_local("80")
40+
c = Connection("web1", gateway="cmd") # $getCommand="cmd"
41+
c.forward_remote("80")
42+
c = Connection("web1", gateway="cmd") # $getCommand="cmd"
43+
c.put(local="local")
44+
c = Connection("web1", gateway="cmd") # $getCommand="cmd"
45+
c.shell()
46+
c = Connection("web1", gateway="cmd") # $getCommand="cmd"
47+
c.sftp()
48+
# no call to desired methods so it is safe
49+
c = Connection("web1", gateway="cmd")
2550

2651
################################################################################
2752
# SerialGroup
@@ -30,18 +55,19 @@
3055

3156
pool = SerialGroup("web1", "web2", "web3")
3257
pool.run("cmd1; cmd2") # $getCommand="cmd1; cmd2"
58+
pool.sudo("cmd1; cmd2") # $getCommand="cmd1; cmd2"
3359

3460
# fully qualified usage
3561
group.SerialGroup("web1", "web2", "mac1").run("cmd1; cmd2") # $getCommand="cmd1; cmd2"
3662

37-
3863
################################################################################
3964
# ThreadingGroup
4065
################################################################################
4166
results = ThreadingGroup("web1", "web2", "mac1").run("cmd1; cmd2") # $getCommand="cmd1; cmd2"
4267

4368
pool = ThreadingGroup("web1", "web2", "web3")
4469
pool.run("cmd1; cmd2") # $getCommand="cmd1; cmd2"
70+
pool.sudo("cmd1; cmd2") # $getCommand="cmd1; cmd2"
4571

4672
# fully qualified usage
4773
group.ThreadingGroup("web1", "web2", "mac1").run("cmd1; cmd2") # $getCommand="cmd1; cmd2"
@@ -57,6 +83,7 @@ def foo(c):
5783
# 'c' is a fabric.connection.Connection
5884
c.run("cmd1; cmd2") # $getCommand="cmd1; cmd2"
5985

86+
6087
# fully qualified usage
6188
@tasks.task
6289
def bar(c):

0 commit comments

Comments
 (0)