Skip to content

Commit b39bcde

Browse files
authored
Merge pull request #2375 from tausbn/python-fix-mutable-value-type-coercion-fp
Python: Don't report mutable parameters that are in fact immutable.
2 parents 231414c + 3c47394 commit b39bcde

File tree

3 files changed

+63
-28
lines changed

3 files changed

+63
-28
lines changed

python/ql/src/Functions/ModificationOfParameterWithDefault.ql

Lines changed: 28 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -15,56 +15,50 @@ import python
1515
import semmle.python.security.Paths
1616

1717
predicate safe_method(string name) {
18-
name = "count" or name = "index" or name = "copy" or name = "get" or name = "has_key" or
19-
name = "items" or name = "keys" or name = "values" or name = "iteritems" or name = "iterkeys" or name = "itervalues"
18+
name = "count" or
19+
name = "index" or
20+
name = "copy" or
21+
name = "get" or
22+
name = "has_key" or
23+
name = "items" or
24+
name = "keys" or
25+
name = "values" or
26+
name = "iteritems" or
27+
name = "iterkeys" or
28+
name = "itervalues"
2029
}
2130

2231
/** Gets the truthiness (non emptyness) of the default of `p` if that value is mutable */
2332
private boolean mutableDefaultValue(Parameter p) {
24-
exists(Dict d |
25-
p.getDefault() = d |
33+
exists(Dict d | p.getDefault() = d |
2634
exists(d.getAKey()) and result = true
2735
or
2836
not exists(d.getAKey()) and result = false
2937
)
3038
or
31-
exists(List l |
32-
p.getDefault() = l |
39+
exists(List l | p.getDefault() = l |
3340
exists(l.getAnElt()) and result = true
3441
or
3542
not exists(l.getAnElt()) and result = false
3643
)
3744
}
3845

39-
4046
class NonEmptyMutableValue extends TaintKind {
41-
NonEmptyMutableValue() {
42-
this = "non-empty mutable value"
43-
}
47+
NonEmptyMutableValue() { this = "non-empty mutable value" }
4448
}
4549

4650
class EmptyMutableValue extends TaintKind {
47-
EmptyMutableValue() {
48-
this = "empty mutable value"
49-
}
50-
51-
override boolean booleanValue() {
52-
result = false
53-
}
51+
EmptyMutableValue() { this = "empty mutable value" }
5452

53+
override boolean booleanValue() { result = false }
5554
}
5655

5756
class MutableDefaultValue extends TaintSource {
58-
5957
boolean nonEmpty;
6058

61-
MutableDefaultValue() {
62-
nonEmpty = mutableDefaultValue(this.(NameNode).getNode())
63-
}
59+
MutableDefaultValue() { nonEmpty = mutableDefaultValue(this.(NameNode).getNode()) }
6460

65-
override string toString() {
66-
result = "mutable default value"
67-
}
61+
override string toString() { result = "mutable default value" }
6862

6963
override predicate isSourceOf(TaintKind kind) {
7064
nonEmpty = false and kind instanceof EmptyMutableValue
@@ -73,14 +67,19 @@ class MutableDefaultValue extends TaintSource {
7367
}
7468
}
7569

70+
private ClassValue mutable_class() {
71+
result = Value::named("list") or
72+
result = Value::named("dict")
73+
}
74+
7675
class Mutation extends TaintSink {
7776
Mutation() {
7877
exists(AugAssign a | a.getTarget().getAFlowNode() = this)
7978
or
80-
exists(Call c, Attribute a |
81-
c.getFunc() = a |
79+
exists(Call c, Attribute a | c.getFunc() = a |
8280
a.getObject().getAFlowNode() = this and
83-
not safe_method(a.getName())
81+
not safe_method(a.getName()) and
82+
this.(ControlFlowNode).pointsTo().getClass() = mutable_class()
8483
)
8584
}
8685

@@ -93,4 +92,5 @@ class Mutation extends TaintSink {
9392

9493
from TaintedPathSource src, TaintedPathSink sink
9594
where src.flowsTo(sink)
96-
select sink.getSink(), src, sink, "$@ flows to here and is mutated.", src.getSource(), "Default value"
95+
select sink.getSink(), src, sink, "$@ flows to here and is mutated.", src.getSource(),
96+
"Default value"

python/ql/test/query-tests/Functions/general/ModificationOfParameterWithDefault.expected

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,18 @@ edges
77
| functions_test.py:157:27:157:27 | empty mutable value | functions_test.py:159:21:159:21 | empty mutable value |
88
| functions_test.py:158:25:158:25 | empty mutable value | functions_test.py:151:25:151:25 | empty mutable value |
99
| functions_test.py:159:21:159:21 | empty mutable value | functions_test.py:154:21:154:21 | empty mutable value |
10+
| functions_test.py:175:28:175:28 | non-empty mutable value | functions_test.py:179:9:179:9 | non-empty mutable value |
11+
| functions_test.py:175:28:175:28 | non-empty mutable value | functions_test.py:181:9:181:9 | non-empty mutable value |
12+
| functions_test.py:188:18:188:18 | non-empty mutable value | functions_test.py:189:28:189:28 | non-empty mutable value |
13+
| functions_test.py:189:28:189:28 | non-empty mutable value | functions_test.py:175:28:175:28 | non-empty mutable value |
14+
| functions_test.py:191:18:191:18 | non-empty mutable value | functions_test.py:192:28:192:28 | non-empty mutable value |
15+
| functions_test.py:192:28:192:28 | non-empty mutable value | functions_test.py:175:28:175:28 | non-empty mutable value |
1016
#select
1117
| functions_test.py:40:5:40:5 | x | functions_test.py:39:9:39:9 | empty mutable value | functions_test.py:40:5:40:5 | empty mutable value | $@ flows to here and is mutated. | functions_test.py:39:9:39:9 | x | Default value |
1218
| functions_test.py:134:5:134:5 | x | functions_test.py:133:15:133:15 | empty mutable value | functions_test.py:134:5:134:5 | empty mutable value | $@ flows to here and is mutated. | functions_test.py:133:15:133:15 | x | Default value |
1319
| functions_test.py:152:5:152:5 | x | functions_test.py:157:27:157:27 | empty mutable value | functions_test.py:152:5:152:5 | empty mutable value | $@ flows to here and is mutated. | functions_test.py:157:27:157:27 | y | Default value |
1420
| functions_test.py:155:5:155:5 | x | functions_test.py:157:27:157:27 | empty mutable value | functions_test.py:155:5:155:5 | empty mutable value | $@ flows to here and is mutated. | functions_test.py:157:27:157:27 | y | Default value |
21+
| functions_test.py:179:9:179:9 | x | functions_test.py:188:18:188:18 | non-empty mutable value | functions_test.py:179:9:179:9 | non-empty mutable value | $@ flows to here and is mutated. | functions_test.py:188:18:188:18 | x | Default value |
22+
| functions_test.py:179:9:179:9 | x | functions_test.py:191:18:191:18 | non-empty mutable value | functions_test.py:179:9:179:9 | non-empty mutable value | $@ flows to here and is mutated. | functions_test.py:191:18:191:18 | x | Default value |
23+
| functions_test.py:181:9:181:9 | x | functions_test.py:188:18:188:18 | non-empty mutable value | functions_test.py:181:9:181:9 | non-empty mutable value | $@ flows to here and is mutated. | functions_test.py:188:18:188:18 | x | Default value |
24+
| functions_test.py:181:9:181:9 | x | functions_test.py:191:18:191:18 | non-empty mutable value | functions_test.py:181:9:181:9 | non-empty mutable value | $@ flows to here and is mutated. | functions_test.py:191:18:191:18 | x | Default value |

python/ql/test/query-tests/Functions/general/functions_test.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,3 +168,28 @@ def issue1143(expr, param=[]):
168168
return result
169169
for i in param:
170170
param.remove(i) # Mutation here
171+
172+
173+
# Type guarding of modification of parameter with default:
174+
175+
def do_stuff_based_on_type(x):
176+
if isinstance(x, str):
177+
x = x.split()
178+
elif isinstance(x, dict):
179+
x.setdefault('foo', 'bar')
180+
elif isinstance(x, list):
181+
x.append(5)
182+
elif isinstance(x, tuple):
183+
x = x.unknown_method()
184+
185+
def str_default(x="hello world"):
186+
do_stuff_based_on_type(x)
187+
188+
def dict_default(x={'baz':'quux'}):
189+
do_stuff_based_on_type(x)
190+
191+
def list_default(x=[1,2,3,4]):
192+
do_stuff_based_on_type(x)
193+
194+
def tuple_default(x=(1,2)):
195+
do_stuff_based_on_type(x)

0 commit comments

Comments
 (0)