Skip to content

Commit 1a7570e

Browse files
authored
Merge pull request github#3563 from RasmusWL/python-fabric-execute
Approved by tausbn
2 parents 5daf1db + 1ff369f commit 1a7570e

File tree

8 files changed

+141
-0
lines changed

8 files changed

+141
-0
lines changed

python/ql/src/Security/CWE-078/CommandInjection.ql

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ class CommandInjectionConfiguration extends TaintTracking::Configuration {
3232

3333
override predicate isExtension(TaintTracking::Extension extension) {
3434
extension instanceof FirstElementFlow
35+
or
36+
extension instanceof FabricExecuteExtension
3537
}
3638
}
3739

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

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,3 +231,41 @@ class FabricV1Commands extends CommandSink {
231231

232232
override predicate sinks(TaintKind kind) { kind instanceof ExternalStringKind }
233233
}
234+
235+
/**
236+
* An extension that propagates taint from the arguments of `fabric.api.execute(func, arg0, arg1, ...)`
237+
* to the parameters of `func`, since this will call `func(arg0, arg1, ...)`.
238+
*/
239+
class FabricExecuteExtension extends DataFlowExtension::DataFlowNode {
240+
CallNode call;
241+
242+
FabricExecuteExtension() {
243+
call = Value::named("fabric.api.execute").getACall() and
244+
(
245+
this = call.getArg(any(int i | i > 0))
246+
or
247+
this = call.getArgByName(any(string s | not s = "task"))
248+
)
249+
}
250+
251+
override ControlFlowNode getASuccessorNode(TaintKind fromkind, TaintKind tokind) {
252+
tokind = fromkind and
253+
exists(CallableValue func |
254+
(
255+
call.getArg(0).pointsTo(func)
256+
or
257+
call.getArgByName("task").pointsTo(func)
258+
) and
259+
exists(int i |
260+
// execute(func, arg0, arg1) => func(arg0, arg1)
261+
this = call.getArg(i) and
262+
result = func.getParameter(i - 1)
263+
)
264+
or
265+
exists(string name |
266+
this = call.getArgByName(name) and
267+
result = func.getParameterByName(name)
268+
)
269+
)
270+
}
271+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import python
2+
import semmle.python.dataflow.TaintTracking
3+
import semmle.python.security.strings.Untrusted
4+
import semmle.python.security.injection.Command
5+
6+
class SimpleSource extends TaintSource {
7+
SimpleSource() { this.(NameNode).getId() = "TAINTED_STRING" }
8+
9+
override predicate isSourceOf(TaintKind kind) { kind instanceof ExternalStringKind }
10+
11+
override string toString() { result = "taint source" }
12+
}
13+
14+
class FabricExecuteTestConfiguration extends TaintTracking::Configuration {
15+
FabricExecuteTestConfiguration() { this = "FabricExecuteTestConfiguration" }
16+
17+
override predicate isSource(TaintTracking::Source source) { source instanceof SimpleSource }
18+
19+
override predicate isSink(TaintTracking::Sink sink) { sink instanceof CommandSink }
20+
21+
override predicate isExtension(TaintTracking::Extension extension) {
22+
extension instanceof FabricExecuteExtension
23+
}
24+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
| test.py:8 | ok | unsafe | cmd | externally controlled string |
2+
| test.py:8 | ok | unsafe | cmd2 | externally controlled string |
3+
| test.py:9 | ok | unsafe | safe_arg | <NO TAINT> |
4+
| test.py:9 | ok | unsafe | safe_optional | <NO TAINT> |
5+
| test.py:16 | ok | unsafe | cmd | externally controlled string |
6+
| test.py:16 | ok | unsafe | cmd2 | externally controlled string |
7+
| test.py:17 | ok | unsafe | safe_arg | <NO TAINT> |
8+
| test.py:17 | ok | unsafe | safe_optional | <NO TAINT> |
9+
| test.py:23 | ok | some_http_handler | cmd | externally controlled string |
10+
| test.py:23 | ok | some_http_handler | cmd2 | externally controlled string |
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import python
2+
import semmle.python.security.TaintTracking
3+
import semmle.python.web.HttpRequest
4+
import semmle.python.security.strings.Untrusted
5+
6+
import Taint
7+
8+
from
9+
Call call, Expr arg, boolean expected_taint, boolean has_taint, string test_res,
10+
string taint_string
11+
where
12+
call.getLocation().getFile().getShortName() = "test.py" and
13+
(
14+
call.getFunc().(Name).getId() = "ensure_tainted" and
15+
expected_taint = true
16+
or
17+
call.getFunc().(Name).getId() = "ensure_not_tainted" and
18+
expected_taint = false
19+
) and
20+
arg = call.getAnArg() and
21+
(
22+
not exists(TaintedNode tainted | tainted.getAstNode() = arg) and
23+
taint_string = "<NO TAINT>" and
24+
has_taint = false
25+
or
26+
exists(TaintedNode tainted | tainted.getAstNode() = arg |
27+
taint_string = tainted.getTaintKind().toString()
28+
) and
29+
has_taint = true
30+
) and
31+
if expected_taint = has_taint then test_res = "ok " else test_res = "fail"
32+
// if expected_taint = has_taint then test_res = "✓" else test_res = "✕"
33+
select arg.getLocation().toString(), test_res, call.getScope().(Function).getName(), arg.toString(),
34+
taint_string
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: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
"""Test that shows fabric.api.execute propagates taint"""
2+
3+
from fabric.api import run, execute
4+
5+
6+
def unsafe(cmd, safe_arg, cmd2=None, safe_optional=5):
7+
run('./venv/bin/activate && {}'.format(cmd))
8+
ensure_tainted(cmd, cmd2)
9+
ensure_not_tainted(safe_arg, safe_optional)
10+
11+
12+
class Foo(object):
13+
14+
def unsafe(self, cmd, safe_arg, cmd2=None, safe_optional=5):
15+
run('./venv/bin/activate && {}'.format(cmd))
16+
ensure_tainted(cmd, cmd2)
17+
ensure_not_tainted(safe_arg, safe_optional)
18+
19+
20+
def some_http_handler():
21+
cmd = TAINTED_STRING
22+
cmd2 = TAINTED_STRING
23+
ensure_tainted(cmd, cmd2)
24+
25+
execute(unsafe, cmd=cmd, safe_arg='safe_arg', cmd2=cmd2)
26+
27+
foo = Foo()
28+
execute(foo.unsafe, cmd, 'safe_arg', cmd2)

python/ql/test/query-tests/Security/lib/fabric/api.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,7 @@ def sudo(command, shell=True, pty=True, combine_stderr=None, user=None,
2323
quiet=False, warn_only=False, stdout=None, stderr=None, group=None,
2424
timeout=None, shell_escape=None, capture_buffer_size=None):
2525
pass
26+
27+
# https://github.com/fabric/fabric/blob/1.14/fabric/tasks.py#L281
28+
def execute(task, *args, **kwargs):
29+
pass

0 commit comments

Comments
 (0)