Skip to content

Commit 1737d08

Browse files
authored
Merge pull request #9579 from yoff/python/more-logic-tests
Python: Improve `BarrierGuard`
2 parents b5d4a2d + 6087bc6 commit 1737d08

File tree

5 files changed

+97
-17
lines changed

5 files changed

+97
-17
lines changed

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

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -527,16 +527,55 @@ class StarPatternElementNode extends Node, TStarPatternElementNode {
527527
override Location getLocation() { result = consumer.getLocation() }
528528
}
529529

530+
/**
531+
* Gets a node that controls whether other nodes are evaluated.
532+
*
533+
* In the base case, this is the last node of `conditionBlock`, and `flipped` is `false`.
534+
* This definition accounts for (short circuting) `and`- and `or`-expressions, as the structure
535+
* of basic blocks will reflect their semantics.
536+
*
537+
* However, in the program
538+
* ```python
539+
* if not is_safe(path):
540+
* return
541+
* ```
542+
* the last node in the `ConditionBlock` is `not is_safe(path)`.
543+
*
544+
* We would like to consider also `is_safe(path)` a guard node, albeit with `flipped` being `true`.
545+
* Thus we recurse through `not`-expressions.
546+
*/
547+
ControlFlowNode guardNode(ConditionBlock conditionBlock, boolean flipped) {
548+
// Base case: the last node truly does determine which successor is chosen
549+
result = conditionBlock.getLastNode() and
550+
flipped = false
551+
or
552+
// Recursive case: if a guard node is a `not`-expression,
553+
// the operand is also a guard node, but with inverted polarity.
554+
exists(UnaryExprNode notNode |
555+
result = notNode.getOperand() and
556+
notNode.getNode().getOp() instanceof Not
557+
|
558+
notNode = guardNode(conditionBlock, flipped.booleanNot())
559+
)
560+
}
561+
530562
/**
531563
* A node that controls whether other nodes are evaluated.
564+
*
565+
* The field `flipped` allows us to match `GuardNode`s underneath
566+
* `not`-expressions and still choose the appropriate branch.
532567
*/
533568
class GuardNode extends ControlFlowNode {
534569
ConditionBlock conditionBlock;
570+
boolean flipped;
535571

536-
GuardNode() { this = conditionBlock.getLastNode() }
572+
GuardNode() { this = guardNode(conditionBlock, flipped) }
537573

538574
/** Holds if this guard controls block `b` upon evaluating to `branch`. */
539-
predicate controlsBlock(BasicBlock b, boolean branch) { conditionBlock.controls(b, branch) }
575+
predicate controlsBlock(BasicBlock b, boolean branch) {
576+
branch in [true, false] and
577+
conditionBlock.controls(b, branch.booleanXor(flipped))
578+
}
540579
}
541580

542581
/**

python/ql/test/experimental/dataflow/tainttracking/commonSanitizer/test_string_const_compare.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ def test_non_eq2():
5050
if not ts == "safe":
5151
ensure_tainted(ts) # $ tainted
5252
else:
53-
ensure_not_tainted(ts) # $ SPURIOUS: tainted
53+
ensure_not_tainted(ts)
5454

5555

5656
def test_in_list():
@@ -157,7 +157,7 @@ def test_not_in2():
157157
if not ts in ["safe", "also_safe"]:
158158
ensure_tainted(ts) # $ tainted
159159
else:
160-
ensure_not_tainted(ts) # $ SPURIOUS: tainted
160+
ensure_not_tainted(ts)
161161

162162

163163
def is_safe(x):

python/ql/test/experimental/dataflow/tainttracking/customSanitizer/InlineTaintTest.expected

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,20 @@ isSanitizer
66
| TestTaintTrackingConfiguration | test.py:34:39:34:39 | ControlFlowNode for s |
77
| TestTaintTrackingConfiguration | test.py:52:28:52:28 | ControlFlowNode for s |
88
| TestTaintTrackingConfiguration | test.py:66:10:66:29 | ControlFlowNode for emulated_escaping() |
9-
| TestTaintTrackingConfiguration | test_logical.py:30:28:30:28 | ControlFlowNode for s |
10-
| TestTaintTrackingConfiguration | test_logical.py:45:28:45:28 | ControlFlowNode for s |
11-
| TestTaintTrackingConfiguration | test_logical.py:50:28:50:28 | ControlFlowNode for s |
12-
| TestTaintTrackingConfiguration | test_logical.py:89:28:89:28 | ControlFlowNode for s |
13-
| TestTaintTrackingConfiguration | test_logical.py:100:28:100:28 | ControlFlowNode for s |
14-
| TestTaintTrackingConfiguration | test_logical.py:145:28:145:28 | ControlFlowNode for s |
9+
| TestTaintTrackingConfiguration | test_logical.py:33:28:33:28 | ControlFlowNode for s |
10+
| TestTaintTrackingConfiguration | test_logical.py:40:28:40:28 | ControlFlowNode for s |
11+
| TestTaintTrackingConfiguration | test_logical.py:48:28:48:28 | ControlFlowNode for s |
12+
| TestTaintTrackingConfiguration | test_logical.py:53:28:53:28 | ControlFlowNode for s |
13+
| TestTaintTrackingConfiguration | test_logical.py:92:28:92:28 | ControlFlowNode for s |
14+
| TestTaintTrackingConfiguration | test_logical.py:103:28:103:28 | ControlFlowNode for s |
15+
| TestTaintTrackingConfiguration | test_logical.py:111:28:111:28 | ControlFlowNode for s |
16+
| TestTaintTrackingConfiguration | test_logical.py:130:28:130:28 | ControlFlowNode for s |
17+
| TestTaintTrackingConfiguration | test_logical.py:137:28:137:28 | ControlFlowNode for s |
1518
| TestTaintTrackingConfiguration | test_logical.py:148:28:148:28 | ControlFlowNode for s |
16-
| TestTaintTrackingConfiguration | test_logical.py:155:28:155:28 | ControlFlowNode for s |
19+
| TestTaintTrackingConfiguration | test_logical.py:151:28:151:28 | ControlFlowNode for s |
20+
| TestTaintTrackingConfiguration | test_logical.py:158:28:158:28 | ControlFlowNode for s |
21+
| TestTaintTrackingConfiguration | test_logical.py:167:24:167:24 | ControlFlowNode for s |
22+
| TestTaintTrackingConfiguration | test_logical.py:176:24:176:24 | ControlFlowNode for s |
23+
| TestTaintTrackingConfiguration | test_logical.py:185:24:185:24 | ControlFlowNode for s |
24+
| TestTaintTrackingConfiguration | test_logical.py:193:24:193:24 | ControlFlowNode for s |
1725
| TestTaintTrackingConfiguration | test_reference.py:31:28:31:28 | ControlFlowNode for s |

python/ql/test/experimental/dataflow/tainttracking/customSanitizer/InlineTaintTest.ql

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ predicate isSafeCheck(DataFlow::GuardNode g, ControlFlowNode node, boolean branc
66
branch = true
77
}
88

9+
predicate isUnsafeCheck(DataFlow::GuardNode g, ControlFlowNode node, boolean branch) {
10+
g.(CallNode).getNode().getFunc().(Name).getId() in ["is_unsafe", "emulated_is_unsafe"] and
11+
node = g.(CallNode).getAnArg() and
12+
branch = false
13+
}
14+
915
class CustomSanitizerOverrides extends TestTaintTrackingConfiguration {
1016
override predicate isSanitizer(DataFlow::Node node) {
1117
exists(Call call |
@@ -16,6 +22,8 @@ class CustomSanitizerOverrides extends TestTaintTrackingConfiguration {
1622
node.asExpr().(Call).getFunc().(Name).getId() = "emulated_escaping"
1723
or
1824
node = DataFlow::BarrierGuard<isSafeCheck/3>::getABarrierNode()
25+
or
26+
node = DataFlow::BarrierGuard<isUnsafeCheck/3>::getABarrierNode()
1927
}
2028
}
2129

python/ql/test/experimental/dataflow/tainttracking/customSanitizer/test_logical.py

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ def random_choice():
2222
def is_safe(arg):
2323
return arg == "safe"
2424

25+
def is_unsafe(arg):
26+
return arg == TAINTED_STRING
27+
2528

2629
def test_basic():
2730
s = TAINTED_STRING
@@ -34,7 +37,7 @@ def test_basic():
3437
if not is_safe(s):
3538
ensure_tainted(s) # $ tainted
3639
else:
37-
ensure_not_tainted(s) # $ SPURIOUS: tainted
40+
ensure_not_tainted(s)
3841

3942

4043
def test_if_in_depth():
@@ -105,7 +108,7 @@ def test_and():
105108
ensure_tainted(s) # $ tainted
106109
else:
107110
# cannot be tainted
108-
ensure_not_tainted(s) # $ SPURIOUS: tainted
111+
ensure_not_tainted(s)
109112

110113

111114
def test_tricky():
@@ -124,14 +127,14 @@ def test_nesting_not():
124127
s = TAINTED_STRING
125128

126129
if not(not(is_safe(s))):
127-
ensure_not_tainted(s) # $ SPURIOUS: tainted
130+
ensure_not_tainted(s)
128131
else:
129132
ensure_tainted(s) # $ tainted
130133

131134
if not(not(not(is_safe(s)))):
132135
ensure_tainted(s) # $ tainted
133136
else:
134-
ensure_not_tainted(s) # $ SPURIOUS: tainted
137+
ensure_not_tainted(s)
135138

136139

137140
# Adding `and True` makes the sanitizer trigger when it would otherwise not. See output in
@@ -161,7 +164,16 @@ def test_with_return():
161164
if not is_safe(s):
162165
return
163166

164-
ensure_not_tainted(s) # $ SPURIOUS: tainted
167+
ensure_not_tainted(s)
168+
169+
170+
def test_with_return_neg():
171+
s = TAINTED_STRING
172+
173+
if is_unsafe(s):
174+
return
175+
176+
ensure_not_tainted(s)
165177

166178

167179
def test_with_exception():
@@ -170,7 +182,15 @@ def test_with_exception():
170182
if not is_safe(s):
171183
raise Exception("unsafe")
172184

173-
ensure_not_tainted(s) # $ SPURIOUS: tainted
185+
ensure_not_tainted(s)
186+
187+
def test_with_exception_neg():
188+
s = TAINTED_STRING
189+
190+
if is_unsafe(s):
191+
raise Exception("unsafe")
192+
193+
ensure_not_tainted(s)
174194

175195
# Make tests runable
176196

@@ -182,7 +202,12 @@ def test_with_exception():
182202
test_nesting_not()
183203
test_nesting_not_with_and_true()
184204
test_with_return()
205+
test_with_return_neg()
185206
try:
186207
test_with_exception()
187208
except:
188209
pass
210+
try:
211+
test_with_exception_neg()
212+
except:
213+
pass

0 commit comments

Comments
 (0)