Skip to content

Commit 1e31416

Browse files
authored
Merge pull request #7031 from yoff/python/taint-through-with
Python: Taint through `async with`
2 parents c708b6b + ac5a46f commit 1e31416

File tree

7 files changed

+169
-15
lines changed

7 files changed

+169
-15
lines changed

python/ql/lib/semmle/python/dataflow/new/internal/TaintTrackingPrivate.qll

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ private module Cached {
5353
DataFlowPrivate::iterableUnpackingStoreStep(nodeFrom, _, nodeTo)
5454
or
5555
awaitStep(nodeFrom, nodeTo)
56+
or
57+
asyncWithStep(nodeFrom, nodeTo)
5658
}
5759
}
5860

@@ -211,3 +213,24 @@ predicate copyStep(DataFlow::CfgNode nodeFrom, DataFlow::CfgNode nodeTo) {
211213
predicate awaitStep(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) {
212214
nodeTo.asExpr().(Await).getValue() = nodeFrom.asExpr()
213215
}
216+
217+
/**
218+
* Holds if taint can flow from `nodeFrom` to `nodeTo` inside an `async with` statement.
219+
*
220+
* For example in
221+
* ```python
222+
* async with open("foo") as f:
223+
* ```
224+
* the variable `f` is tainted if the result of `open("foo")` is tainted.
225+
*/
226+
predicate asyncWithStep(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) {
227+
exists(With with, ControlFlowNode contextManager, ControlFlowNode var |
228+
nodeFrom.(DataFlow::CfgNode).getNode() = contextManager and
229+
nodeTo.(DataFlow::EssaNode).getVar().getDefinition().(WithDefinition).getDefiningNode() = var and
230+
// see `with_flow` in `python/ql/src/semmle/python/dataflow/Implementation.qll`
231+
with.getContextExpr() = contextManager.getNode() and
232+
with.getOptionalVars() = var.getNode() and
233+
with.isAsync() and
234+
contextManager.strictlyDominates(var)
235+
)
236+
}

python/ql/test/experimental/dataflow/tainttracking/defaultAdditionalTaintStep/test_async.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ async def test_async_with():
3131
ctx = AsyncContext()
3232
taint(ctx)
3333
async with ctx as tainted:
34-
ensure_tainted(tainted) # $ MISSING: tainted
34+
ensure_tainted(tainted) # $ tainted
3535

3636

3737
class AsyncIter:
@@ -45,7 +45,7 @@ async def test_async_for():
4545
iter = AsyncIter()
4646
taint(iter)
4747
async for tainted in iter:
48-
ensure_tainted(tainted) # $ MISSING: tainted
48+
ensure_tainted(tainted) # $ tainted
4949

5050

5151

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Add taintlib to PATH so it can be imported during runtime without any hassle
2+
import sys; import os; sys.path.append(os.path.dirname(os.path.dirname((__file__))))
3+
from taintlib import *
4+
5+
# This has no runtime impact, but allows autocomplete to work
6+
from typing import TYPE_CHECKING
7+
if TYPE_CHECKING:
8+
from ..taintlib import *
9+
10+
11+
# Actual tests
12+
13+
class Iter:
14+
def __iter__(self):
15+
return self
16+
17+
def __next__(self):
18+
raise StopIteration
19+
20+
def test_for():
21+
iter = Iter()
22+
taint(iter)
23+
for tainted in iter:
24+
ensure_tainted(tainted) # $ tainted
25+
26+
27+
28+
# Make tests runable
29+
30+
test_for()
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# Add taintlib to PATH so it can be imported during runtime without any hassle
2+
import sys; import os; sys.path.append(os.path.dirname(os.path.dirname((__file__))))
3+
from taintlib import *
4+
5+
# This has no runtime impact, but allows autocomplete to work
6+
from typing import TYPE_CHECKING
7+
if TYPE_CHECKING:
8+
from ..taintlib import *
9+
10+
11+
# Actual tests
12+
13+
class Context:
14+
def __enter__(self):
15+
return ""
16+
17+
def __exit__(self, exc_type, exc, tb):
18+
pass
19+
20+
def test_with():
21+
ctx = Context()
22+
taint(ctx)
23+
with ctx as tainted:
24+
ensure_tainted(tainted) # $ tainted
25+
26+
class Context_taint:
27+
def __enter__(self):
28+
return TAINTED_STRING
29+
30+
def __exit__(self, exc_type, exc, tb):
31+
pass
32+
33+
def test_with_taint():
34+
ctx = Context_taint()
35+
with ctx as tainted:
36+
ensure_tainted(tainted) # $ MISSING: tainted
37+
38+
39+
class Context_arg:
40+
def __init__(self, arg):
41+
self.arg = arg
42+
43+
def __enter__(self):
44+
return self.arg
45+
46+
def __exit__(self, exc_type, exc, tb):
47+
pass
48+
49+
def test_with_arg():
50+
ctx = Context_arg(TAINTED_STRING)
51+
with ctx as tainted:
52+
ensure_tainted(tainted) # $ tainted
53+
54+
55+
56+
# Make tests runable
57+
58+
test_with()
59+
test_with_taint()
60+
test_with_arg()

python/ql/test/experimental/meta/InlineTaintTest.qll

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -30,24 +30,36 @@ DataFlow::Node shouldNotBeTainted() {
3030
)
3131
}
3232

33-
class TestTaintTrackingConfiguration extends TaintTracking::Configuration {
34-
TestTaintTrackingConfiguration() { this = "TestTaintTrackingConfiguration" }
33+
// this module allows the configuration to be imported in other `.ql` files without the
34+
// top level query predicates of this file coming into scope.
35+
module Conf {
36+
class TestTaintTrackingConfiguration extends TaintTracking::Configuration {
37+
TestTaintTrackingConfiguration() { this = "TestTaintTrackingConfiguration" }
3538

36-
override predicate isSource(DataFlow::Node source) {
37-
source.asCfgNode().(NameNode).getId() in [
38-
"TAINTED_STRING", "TAINTED_BYTES", "TAINTED_LIST", "TAINTED_DICT"
39-
]
40-
or
41-
source instanceof RemoteFlowSource
42-
}
39+
override predicate isSource(DataFlow::Node source) {
40+
source.asCfgNode().(NameNode).getId() in [
41+
"TAINTED_STRING", "TAINTED_BYTES", "TAINTED_LIST", "TAINTED_DICT"
42+
]
43+
or
44+
// User defined sources
45+
exists(CallNode call |
46+
call.getFunction().(NameNode).getId() = "taint" and
47+
source.(DataFlow::CfgNode).getNode() = call.getAnArg()
48+
)
49+
or
50+
source instanceof RemoteFlowSource
51+
}
4352

44-
override predicate isSink(DataFlow::Node sink) {
45-
sink = shouldBeTainted()
46-
or
47-
sink = shouldNotBeTainted()
53+
override predicate isSink(DataFlow::Node sink) {
54+
sink = shouldBeTainted()
55+
or
56+
sink = shouldNotBeTainted()
57+
}
4858
}
4959
}
5060

61+
import Conf
62+
5163
class InlineTaintTest extends InlineExpectationsTest {
5264
InlineTaintTest() { this = "InlineTaintTest" }
5365

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
edges
2+
nodes
3+
subpaths
4+
#select
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/**
2+
* @kind path-problem
3+
*/
4+
5+
// This query is for debugging InlineTaintTestFailures.
6+
// The intended usage is
7+
// 1. load the database of the failing test
8+
// 2. run this query to see actual paths
9+
// 3. if necessary, look at partial paths by (un)commenting appropriate lines
10+
import python
11+
import semmle.python.dataflow.new.DataFlow
12+
import experimental.meta.InlineTaintTest::Conf
13+
// import DataFlow::PartialPathGraph
14+
import DataFlow::PathGraph
15+
16+
class Conf extends TestTaintTrackingConfiguration {
17+
override int explorationLimit() { result = 5 }
18+
}
19+
20+
// from Conf config, DataFlow::PartialPathNode source, DataFlow::PartialPathNode sink
21+
// where config.hasPartialFlow(source, sink, _)
22+
from Conf config, DataFlow::PathNode source, DataFlow::PathNode sink
23+
where config.hasFlowPath(source, sink)
24+
select sink.getNode(), source, sink, "This node receives taint from $@.", source.getNode(),
25+
"this source"

0 commit comments

Comments
 (0)