Skip to content

Commit a57eada

Browse files
committed
Python: Model fabric/invoke command injection sinks
1 parent d475bb9 commit a57eada

File tree

14 files changed

+257
-0
lines changed

14 files changed

+257
-0
lines changed

python/ql/src/semmle/python/security/injection/Command.qll

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,3 +123,107 @@ class OsCommandFirstArgument extends CommandSink {
123123
}
124124

125125
}
126+
127+
// -------------------------------------------------------------------------- //
128+
// Modeling of the 'invoke' package and 'fabric' package (v 2.x)
129+
//
130+
// Since fabric build so closely upon invoke, we model them together to avoid
131+
// duplication
132+
// -------------------------------------------------------------------------- //
133+
134+
/** A taint sink that is potentially vulnerable to malicious shell commands.
135+
* The `vuln` in `invoke.run(vuln, ...)` and similar calls.
136+
*/
137+
class InvokeRun extends CommandSink {
138+
InvokeRun() {
139+
this = Value::named("invoke.run").(FunctionValue).getArgumentForCall(_, 0)
140+
or
141+
this = Value::named("invoke.sudo").(FunctionValue).getArgumentForCall(_, 0)
142+
}
143+
144+
override string toString() { result = "InvokeRun" }
145+
146+
override predicate sinks(TaintKind kind) {
147+
kind instanceof ExternalStringKind
148+
}
149+
}
150+
151+
/** Internal TaintKind to track the invoke.Context instance passed to functions
152+
* marked with @invoke.task
153+
*/
154+
private class InvokeContextArg extends TaintKind {
155+
InvokeContextArg() { this = "InvokeContextArg" }
156+
}
157+
158+
/** Internal TaintSource to track the context passed to functions marked with @invoke.task */
159+
private class InvokeContextArgSource extends TaintSource {
160+
InvokeContextArgSource() {
161+
exists(Function f, Expr decorator |
162+
count(f.getADecorator()) = 1 and
163+
(
164+
decorator = f.getADecorator() and not decorator instanceof Call
165+
or
166+
decorator = f.getADecorator().(Call).getFunc()
167+
168+
) and
169+
(
170+
decorator.pointsTo(Value::named("invoke.task"))
171+
or
172+
decorator.pointsTo(Value::named("fabric.task"))
173+
)
174+
|
175+
this.(ControlFlowNode).getNode() = f.getArg(0)
176+
)
177+
}
178+
179+
override predicate isSourceOf(TaintKind kind) {
180+
kind instanceof InvokeContextArg
181+
}
182+
}
183+
184+
/** A taint sink that is potentially vulnerable to malicious shell commands.
185+
* The `vuln` in `invoke.Context().run(vuln, ...)` and similar calls.
186+
*/
187+
class InvokeContextRun extends CommandSink {
188+
InvokeContextRun() {
189+
exists(CallNode call |
190+
any(InvokeContextArg k).taints(call.getFunction().(AttrNode).getObject("run"))
191+
or
192+
call = Value::named("invoke.Context").(ClassValue).lookup("run").getACall()
193+
or
194+
// fabric.connection.Connection is a subtype of invoke.context.Context
195+
// since fabric.Connection.run has a decorator, it doesn't work with FunctionValue :|
196+
// and `Value::named("fabric.Connection").(ClassValue).lookup("run").getACall()` returned no results,
197+
// so here is the hacky solution that works :\
198+
call.getFunction().(AttrNode).getObject("run").pointsTo().getClass() = Value::named("fabric.Connection")
199+
|
200+
this = call.getArg(0)
201+
or
202+
this = call.getArgByName("command")
203+
)
204+
}
205+
206+
override string toString() { result = "InvokeContextRun" }
207+
208+
override predicate sinks(TaintKind kind) {
209+
kind instanceof ExternalStringKind
210+
}
211+
}
212+
213+
/** A taint sink that is potentially vulnerable to malicious shell commands.
214+
* The `vuln` in `fabric.Group().run(vuln, ...)` and similar calls.
215+
*/
216+
class FabricGroupRun extends CommandSink {
217+
FabricGroupRun() {
218+
exists(ClassValue cls |
219+
cls.getASuperType() = Value::named("fabric.Group") and
220+
this = cls.lookup("run").(FunctionValue).getArgumentForCall(_, 1)
221+
)
222+
}
223+
224+
override string toString() { result = "FabricGroupRun" }
225+
226+
override predicate sinks(TaintKind kind) {
227+
kind instanceof ExternalStringKind
228+
}
229+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
| fabric_test.py:10:16:10:25 | InvokeContextRun | externally controlled string |
2+
| fabric_test.py:12:15:12:36 | InvokeContextRun | externally controlled string |
3+
| fabric_test.py:16:45:16:54 | FabricGroupRun | externally controlled string |
4+
| fabric_test.py:21:10:21:13 | FabricGroupRun | externally controlled string |
5+
| fabric_test.py:31:14:31:41 | InvokeContextRun | externally controlled string |
6+
| fabric_test.py:33:15:33:64 | InvokeContextRun | externally controlled string |
7+
| invoke_test.py:8:12:8:21 | InvokeRun | externally controlled string |
8+
| invoke_test.py:9:20:9:40 | InvokeRun | externally controlled string |
9+
| invoke_test.py:12:17:12:24 | InvokeRun | externally controlled string |
10+
| invoke_test.py:13:25:13:32 | InvokeRun | externally controlled string |
11+
| invoke_test.py:17:11:17:40 | InvokeContextRun | externally controlled string |
12+
| invoke_test.py:21:11:21:32 | InvokeContextRun | externally controlled string |
13+
| invoke_test.py:27:11:27:25 | InvokeContextRun | externally controlled string |
14+
| invoke_test.py:32:11:32:25 | InvokeContextRun | externally controlled string |
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import python
2+
import semmle.python.security.injection.Command
3+
import semmle.python.security.strings.Untrusted
4+
5+
from CommandSink sink, TaintKind kind
6+
where sink.sinks(kind)
7+
select sink, kind
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
Copyright (c) 2020 Jeff Forcier.
2+
All rights reserved.
3+
4+
Redistribution and use in source and binary forms, with or without
5+
modification, are permitted provided that the following conditions are met:
6+
7+
* Redistributions of source code must retain the above copyright notice,
8+
this list of conditions and the following disclaimer.
9+
* Redistributions in binary form must reproduce the above copyright notice,
10+
this list of conditions and the following disclaimer in the documentation
11+
and/or other materials provided with the distribution.
12+
13+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
14+
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
15+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
16+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
17+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
18+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
19+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
20+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
21+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
22+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
"""tests for the 'fabric' package (v2.x)
2+
3+
Most of these examples are taken from the fabric documentation: http://docs.fabfile.org/en/2.5/getting-started.html
4+
See fabric-LICENSE for its' license.
5+
"""
6+
7+
from fabric import Connection
8+
9+
c = Connection('web1')
10+
result = c.run('uname -s')
11+
12+
c.run(command='echo run with kwargs')
13+
14+
15+
from fabric import SerialGroup as Group
16+
results = Group('web1', 'web2', 'mac1').run('uname -s')
17+
18+
19+
from fabric import SerialGroup as Group
20+
pool = Group('web1', 'web2', 'web3')
21+
pool.run('ls')
22+
23+
24+
25+
# using the 'fab' command-line tool
26+
27+
from fabric import task
28+
29+
@task
30+
def upload_and_unpack(c):
31+
if c.run('test -f /opt/mydata/myfile', warn=True).failed:
32+
c.put('myfiles.tgz', '/opt/mydata')
33+
c.run('tar -C /opt/mydata -xzvf /opt/mydata/myfiles.tgz')
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
"""tests for the 'invoke' package
2+
3+
see https://www.pyinvoke.org/
4+
"""
5+
6+
import invoke
7+
8+
invoke.run('echo run')
9+
invoke.run(command='echo run with kwarg')
10+
11+
def with_sudo():
12+
invoke.sudo('whoami')
13+
invoke.sudo(command='whoami')
14+
15+
def manual_context():
16+
c = invoke.Context()
17+
c.run('echo run from manual context')
18+
manual_context()
19+
20+
def foo_helper(c):
21+
c.run('echo from foo_helper')
22+
23+
# for use with the 'invoke' command-line tool
24+
@invoke.task
25+
def foo(c):
26+
# 'c' is a invoke.context.Context
27+
c.run('echo task foo')
28+
foo_helper(c)
29+
30+
@invoke.task()
31+
def bar(c):
32+
c.run('echo task bar')
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
semmle-extractor-options: --max-import-depth=2 -p ../../../query-tests/Security/lib/
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .connection import Connection
2+
from .group import Group, SerialGroup, ThreadingGroup
3+
from .tasks import task
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from invoke import Context
2+
3+
@decorator
4+
def opens(method, self, *args, **kwargs):
5+
self.open()
6+
return method(self, *args, **kwargs)
7+
8+
class Connection(Context):
9+
10+
def open(self):
11+
pass
12+
13+
@opens
14+
def run(self, command, **kwargs):
15+
pass
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
class Group(list):
2+
def run(self, *args, **kwargs):
3+
raise NotImplementedError
4+
5+
class SerialGroup(Group):
6+
def run(self, *args, **kwargs):
7+
pass
8+
9+
class ThreadingGroup(Group):
10+
def run(self, *args, **kwargs):
11+
pass

0 commit comments

Comments
 (0)