Skip to content

Commit 27f474f

Browse files
authored
Merge pull request github#4429 from RasmusWL/python-model-invoke
Python: model invoke library
2 parents 8127d9b + 4d9d215 commit 27f474f

File tree

7 files changed

+196
-1
lines changed

7 files changed

+196
-1
lines changed

python/ql/src/experimental/dataflow/internal/DataFlowPublic.qll

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,9 @@ class ParameterNode extends EssaNode {
152152
}
153153

154154
override DataFlowCallable getEnclosingCallable() { this.isParameterOf(result, _) }
155+
156+
/** Gets the `Parameter` this `ParameterNode` represents. */
157+
Parameter getParameter() { result = var.(ParameterDefinition).getParameter() }
155158
}
156159

157160
/**

python/ql/src/experimental/semmle/python/Frameworks.qll

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
* Helper file that imports all framework modeling.
33
*/
44

5-
private import experimental.semmle.python.frameworks.Flask
65
private import experimental.semmle.python.frameworks.Django
6+
private import experimental.semmle.python.frameworks.Flask
7+
private import experimental.semmle.python.frameworks.Invoke
78
private import experimental.semmle.python.frameworks.Stdlib
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
/**
2+
* Provides classes modeling security-relevant aspects of the `invoke` PyPI package.
3+
* See https://www.pyinvoke.org/.
4+
*/
5+
6+
private import python
7+
private import experimental.dataflow.DataFlow
8+
private import experimental.semmle.python.Concepts
9+
10+
/**
11+
* Provides models for the `invoke` PyPI package.
12+
* See https://www.pyinvoke.org/.
13+
*/
14+
private module Invoke {
15+
// ---------------------------------------------------------------------------
16+
// invoke
17+
// ---------------------------------------------------------------------------
18+
/** Gets a reference to the `invoke` module. */
19+
private DataFlow::Node invoke(DataFlow::TypeTracker t) {
20+
t.start() and
21+
result = DataFlow::importNode("invoke")
22+
or
23+
exists(DataFlow::TypeTracker t2 | result = invoke(t2).track(t2, t))
24+
}
25+
26+
/** Gets a reference to the `invoke` module. */
27+
DataFlow::Node invoke() { result = invoke(DataFlow::TypeTracker::end()) }
28+
29+
/**
30+
* Gets a reference to the attribute `attr_name` of the `invoke` module.
31+
* WARNING: Only holds for a few predefined attributes.
32+
*/
33+
private DataFlow::Node invoke_attr(DataFlow::TypeTracker t, string attr_name) {
34+
attr_name in ["run", "sudo", "context", "Context", "task"] and
35+
(
36+
t.start() and
37+
result = DataFlow::importNode("invoke." + attr_name)
38+
or
39+
t.startInAttr(attr_name) and
40+
result = DataFlow::importNode("invoke")
41+
)
42+
or
43+
// Due to bad performance when using normal setup with `invoke_attr(t2, attr_name).track(t2, t)`
44+
// we have inlined that code and forced a join
45+
exists(DataFlow::TypeTracker t2 |
46+
exists(DataFlow::StepSummary summary |
47+
invoke_attr_first_join(t2, attr_name, result, summary) and
48+
t = t2.append(summary)
49+
)
50+
)
51+
}
52+
53+
pragma[nomagic]
54+
private predicate invoke_attr_first_join(
55+
DataFlow::TypeTracker t2, string attr_name, DataFlow::Node res, DataFlow::StepSummary summary
56+
) {
57+
DataFlow::StepSummary::step(invoke_attr(t2, attr_name), res, summary)
58+
}
59+
60+
/**
61+
* Gets a reference to the attribute `attr_name` of the `invoke` module.
62+
* WARNING: Only holds for a few predefined attributes.
63+
*/
64+
private DataFlow::Node invoke_attr(string attr_name) {
65+
result = invoke_attr(DataFlow::TypeTracker::end(), attr_name)
66+
}
67+
68+
/** Provides models for the `invoke` module. */
69+
module invoke {
70+
/** Gets a reference to the `invoke.context` module. */
71+
DataFlow::Node context() { result = invoke_attr("context") }
72+
73+
/** Provides models for the `invoke.context` module */
74+
module context {
75+
/** Provides models for the `invoke.context.Context` class */
76+
module Context {
77+
/** Gets a reference to the `invoke.context.Context` class. */
78+
private DataFlow::Node classRef(DataFlow::TypeTracker t) {
79+
t.start() and
80+
result = DataFlow::importNode("invoke.context.Context")
81+
or
82+
t.startInAttr("Context") and
83+
result = invoke::context()
84+
or
85+
// handle invoke.Context alias
86+
t.start() and
87+
result = invoke_attr("Context")
88+
or
89+
exists(DataFlow::TypeTracker t2 | result = classRef(t2).track(t2, t))
90+
}
91+
92+
/** Gets a reference to the `invoke.context.Context` class. */
93+
DataFlow::Node classRef() { result = classRef(DataFlow::TypeTracker::end()) }
94+
95+
/** Gets a reference to an instance of `invoke.context.Context`. */
96+
private DataFlow::Node instance(DataFlow::TypeTracker t) {
97+
t.start() and
98+
result.asCfgNode().(CallNode).getFunction() =
99+
invoke::context::Context::classRef().asCfgNode()
100+
or
101+
t.start() and
102+
exists(Function func |
103+
func.getADecorator() = invoke_attr("task").asExpr() and
104+
result.(DataFlow::ParameterNode).getParameter() = func.getArg(0)
105+
)
106+
or
107+
exists(DataFlow::TypeTracker t2 | result = instance(t2).track(t2, t))
108+
}
109+
110+
/** Gets a reference to an instance of `invoke.context.Context`. */
111+
DataFlow::Node instance() { result = instance(DataFlow::TypeTracker::end()) }
112+
113+
/** Gets a reference to the `run` or `sudo` methods on a `invoke.context.Context` instance. */
114+
private DataFlow::Node instanceRunMethods(DataFlow::TypeTracker t) {
115+
t.startInAttr(["run", "sudo"]) and
116+
result = invoke::context::Context::instance()
117+
or
118+
exists(DataFlow::TypeTracker t2 | result = instanceRunMethods(t2).track(t2, t))
119+
}
120+
121+
/** Gets a reference to the `run` or `sudo` methods on a `invoke.context.Context` instance. */
122+
DataFlow::Node instanceRunMethods() {
123+
result = instanceRunMethods(DataFlow::TypeTracker::end())
124+
}
125+
}
126+
}
127+
}
128+
129+
/**
130+
* A call to either
131+
* - `invoke.run` or `invoke.sudo` functions (http://docs.pyinvoke.org/en/stable/api/__init__.html)
132+
* - `run` or `sudo` methods on a `invoke.context.Context` instance (http://docs.pyinvoke.org/en/stable/api/context.html#invoke.context.Context.run)
133+
*/
134+
private class InvokeRunCommandCall extends SystemCommandExecution::Range, DataFlow::CfgNode {
135+
override CallNode node;
136+
137+
InvokeRunCommandCall() {
138+
exists(DataFlow::Node callFunction | node.getFunction() = callFunction.asCfgNode() |
139+
callFunction = invoke_attr(["run", "sudo"])
140+
or
141+
callFunction = invoke::context::Context::instanceRunMethods()
142+
)
143+
}
144+
145+
override DataFlow::Node getCommand() {
146+
result.asCfgNode() in [node.getArg(0), node.getArgByName("command")]
147+
}
148+
}
149+
}

python/ql/src/semmle/python/essa/Essa.qll

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -567,6 +567,7 @@ class ParameterDefinition extends EssaNodeDefinition {
567567
exists(Function func | func.getKwarg() = this.getDefiningNode().getNode())
568568
}
569569

570+
/** Gets the `Parameter` this `ParameterDefinition` represents. */
570571
Parameter getParameter() { result = this.getDefiningNode().getNode() }
571572
}
572573

python/ql/test/experimental/library-tests/frameworks/invoke/ConceptsTest.expected

Whitespace-only changes.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import python
2+
import experimental.meta.ConceptsTest
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
"""tests for the 'invoke' package
2+
3+
see https://www.pyinvoke.org/
4+
"""
5+
6+
import invoke
7+
8+
invoke.run("cmd1; cmd2") # $getCommand="cmd1; cmd2"
9+
invoke.run(command="cmd1; cmd2") # $getCommand="cmd1; cmd2"
10+
11+
12+
def with_sudo():
13+
invoke.sudo("cmd1; cmd2") # $getCommand="cmd1; cmd2"
14+
invoke.sudo(command="cmd1; cmd2") # $getCommand="cmd1; cmd2"
15+
16+
17+
def manual_context():
18+
c = invoke.Context()
19+
c.run("cmd1; cmd2") # $getCommand="cmd1; cmd2"
20+
c.sudo("cmd1; cmd2") # $getCommand="cmd1; cmd2"
21+
22+
# invoke.Context is just an alias for invoke.context.Context
23+
c2 = invoke.context.Context()
24+
c2.run("cmd1; cmd2") # $getCommand="cmd1; cmd2"
25+
26+
27+
manual_context()
28+
29+
30+
def foo_helper(c):
31+
c.run("cmd1; cmd2") # $getCommand="cmd1; cmd2"
32+
33+
34+
# for use with the 'invoke' command-line tool
35+
@invoke.task
36+
def foo(c):
37+
# 'c' is a invoke.context.Context
38+
c.run("cmd1; cmd2") # $getCommand="cmd1; cmd2"
39+
foo_helper(c)

0 commit comments

Comments
 (0)