Skip to content

Commit e31143c

Browse files
authored
Merge pull request github#2889 from RasmusWL/python-add-custom-sanitizer-example
Python: Add example for how to write your own sanitizer
2 parents 4bbf462 + 6127d8b commit e31143c

File tree

6 files changed

+274
-0
lines changed

6 files changed

+274
-0
lines changed
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
| MySanitizerHandlingNot | externally controlled string | test.py:13 | Pi(s_0) [true] |
2+
| MySanitizerHandlingNot | externally controlled string | test.py:18 | Pi(s_5) [false] |
3+
| MySanitizerHandlingNot | externally controlled string | test.py:28 | Pi(s_0) [true] |
4+
| MySanitizerHandlingNot | externally controlled string | test.py:34 | Pi(s_10) [true] |
5+
| MySanitizerHandlingNot | externally controlled string | test.py:40 | Pi(s_12) [false] |
6+
| MySanitizerHandlingNot | externally controlled string | test.py:50 | Pi(s_0) [true] |
7+
| MySanitizerHandlingNot | externally controlled string | test.py:56 | Pi(s_10) [true] |
8+
| MySanitizerHandlingNot | externally controlled string | test.py:62 | Pi(s_12) [false] |
9+
| MySanitizerHandlingNot | externally controlled string | test.py:76 | Pi(s_3) [true] |
10+
| MySanitizerHandlingNot | externally controlled string | test.py:82 | Pi(s_0) [true] |
11+
| MySanitizerHandlingNot | externally controlled string | test.py:87 | Pi(s_5) [false] |
12+
| MySanitizerHandlingNot | externally controlled string | test.py:97 | Pi(s_0) [true] |
13+
| MySanitizerHandlingNot | externally controlled string | test.py:102 | Pi(s_7) [true] |
14+
| MySanitizerHandlingNot | externally controlled string | test.py:107 | Pi(s_12) [true] |
15+
| MySimpleSanitizer | externally controlled string | test.py:13 | Pi(s_0) [true] |
16+
| MySimpleSanitizer | externally controlled string | test.py:28 | Pi(s_0) [true] |
17+
| MySimpleSanitizer | externally controlled string | test.py:34 | Pi(s_10) [true] |
18+
| MySimpleSanitizer | externally controlled string | test.py:50 | Pi(s_0) [true] |
19+
| MySimpleSanitizer | externally controlled string | test.py:56 | Pi(s_10) [true] |
20+
| MySimpleSanitizer | externally controlled string | test.py:76 | Pi(s_3) [true] |
21+
| MySimpleSanitizer | externally controlled string | test.py:97 | Pi(s_0) [true] |
22+
| MySimpleSanitizer | externally controlled string | test.py:102 | Pi(s_7) [true] |
23+
| MySimpleSanitizer | externally controlled string | test.py:107 | Pi(s_12) [true] |
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import python
2+
import Taint
3+
4+
from Sanitizer s, TaintKind taint, PyEdgeRefinement test
5+
where s.sanitizingEdge(taint, test)
6+
select s, taint, test.getTest().getLocation().toString(), test.getRepresentation()
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import python
2+
import semmle.python.security.TaintTracking
3+
import semmle.python.security.strings.Untrusted
4+
5+
class SimpleSource extends TaintSource {
6+
SimpleSource() { this.(NameNode).getId() = "TAINTED_STRING" }
7+
8+
override predicate isSourceOf(TaintKind kind) { kind instanceof ExternalStringKind }
9+
10+
override string toString() { result = "taint source" }
11+
}
12+
13+
class MySimpleSanitizer extends Sanitizer {
14+
MySimpleSanitizer() { this = "MySimpleSanitizer" }
15+
16+
/**
17+
* The test `if is_safe(arg):` sanitizes `arg` on its `true` edge.
18+
*
19+
* Can't handle `if not is_safe(arg):` :\ that's why it's called MySimpleSanitizer
20+
*/
21+
override predicate sanitizingEdge(TaintKind taint, PyEdgeRefinement test) {
22+
taint instanceof ExternalStringKind and
23+
exists(CallNode call | test.getTest() = call and test.getSense() = true |
24+
call = Value::named("test.is_safe").getACall() and
25+
test.getInput().getAUse() = call.getAnArg()
26+
)
27+
}
28+
}
29+
30+
class MySanitizerHandlingNot extends Sanitizer {
31+
MySanitizerHandlingNot() { this = "MySanitizerHandlingNot" }
32+
33+
/** The test `if is_safe(arg):` sanitizes `arg` on its `true` edge. */
34+
override predicate sanitizingEdge(TaintKind taint, PyEdgeRefinement test) {
35+
taint instanceof ExternalStringKind and
36+
clears_taint_on_true(test.getTest(), test.getSense(), test)
37+
}
38+
}
39+
40+
/**
41+
* Helper predicate that recurses into any nesting of `not`
42+
*
43+
* To reduce the number of tuples this predicate holds for, we include the `PyEdgeRefinement` and
44+
* ensure that `test` is a part of this `PyEdgeRefinement` (instead of just taking the
45+
* `edge_refinement.getInput().getAUse()` part as a part of the predicate). Without including
46+
* `PyEdgeRefinement` as an argument *any* `CallNode c` to `test.is_safe` would be a result of
47+
* this predicate, since the tuple where `test = c` and `sense = true` would hold.
48+
*/
49+
private predicate clears_taint_on_true(
50+
ControlFlowNode test, boolean sense, PyEdgeRefinement edge_refinement
51+
) {
52+
edge_refinement.getTest().getNode().(Expr).getASubExpression*() = test.getNode() and
53+
(
54+
test = Value::named("test.is_safe").getACall() and
55+
edge_refinement.getInput().getAUse() = test.(CallNode).getAnArg() and
56+
sense = true
57+
or
58+
test.(UnaryExprNode).getNode().getOp() instanceof Not and
59+
exists(ControlFlowNode nested_test |
60+
nested_test = test.(UnaryExprNode).getOperand() and
61+
clears_taint_on_true(nested_test, sense.booleanNot(), edge_refinement)
62+
)
63+
)
64+
}
65+
66+
class TestConfig extends TaintTracking::Configuration {
67+
TestConfig() { this = "TestConfig" }
68+
69+
override predicate isSanitizer(Sanitizer sanitizer) {
70+
sanitizer instanceof MySanitizerHandlingNot
71+
}
72+
73+
override predicate isSource(TaintTracking::Source source) { source instanceof SimpleSource }
74+
75+
override predicate isSink(TaintTracking::Sink sink) { none() }
76+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
| test.py:14 | test_basic | s | <NO TAINT> | ok |
2+
| test.py:16 | test_basic | s | externally controlled string | ok |
3+
| test.py:19 | test_basic | s | externally controlled string | ok |
4+
| test.py:21 | test_basic | s | <NO TAINT> | ok |
5+
| test.py:29 | test_or | s | externally controlled string | ok |
6+
| test.py:31 | test_or | s | externally controlled string | ok |
7+
| test.py:35 | test_or | s | externally controlled string | ok |
8+
| test.py:37 | test_or | s | externally controlled string | ok |
9+
| test.py:41 | test_or | s | externally controlled string | ok |
10+
| test.py:43 | test_or | s | externally controlled string | ok |
11+
| test.py:51 | test_and | s | <NO TAINT> | ok |
12+
| test.py:53 | test_and | s | externally controlled string | ok |
13+
| test.py:57 | test_and | s | externally controlled string | ok |
14+
| test.py:59 | test_and | s | <NO TAINT> | ok |
15+
| test.py:63 | test_and | s | externally controlled string | ok |
16+
| test.py:65 | test_and | s | <NO TAINT> | ok |
17+
| test.py:73 | test_tricky | s | externally controlled string | failure |
18+
| test.py:77 | test_tricky | s_ | externally controlled string | failure |
19+
| test.py:83 | test_nesting_not | s | <NO TAINT> | ok |
20+
| test.py:85 | test_nesting_not | s | externally controlled string | ok |
21+
| test.py:88 | test_nesting_not | s | externally controlled string | ok |
22+
| test.py:90 | test_nesting_not | s | <NO TAINT> | ok |
23+
| test.py:98 | test_nesting_not_with_and_true | s | externally controlled string | ok |
24+
| test.py:100 | test_nesting_not_with_and_true | s | <NO TAINT> | ok |
25+
| test.py:103 | test_nesting_not_with_and_true | s | <NO TAINT> | ok |
26+
| test.py:105 | test_nesting_not_with_and_true | s | externally controlled string | ok |
27+
| test.py:108 | test_nesting_not_with_and_true | s | externally controlled string | ok |
28+
| test.py:110 | test_nesting_not_with_and_true | s | <NO TAINT> | ok |
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import python
2+
import semmle.python.security.TaintTracking
3+
import Taint
4+
5+
from Call call, Expr arg,
6+
boolean expected_taint, boolean has_taint, string test_res,
7+
string taint_string
8+
where
9+
call.getLocation().getFile().getShortName() = "test.py" and
10+
(
11+
call.getFunc().(Name).getId() = "ensure_tainted" and
12+
expected_taint = true
13+
or
14+
call.getFunc().(Name).getId() = "ensure_not_tainted" and
15+
expected_taint = false
16+
) and
17+
arg = call.getAnArg() and
18+
(
19+
not exists(TaintedNode tainted | tainted.getAstNode() = arg) and
20+
taint_string = "<NO TAINT>" and
21+
has_taint = false
22+
or
23+
exists(TaintedNode tainted | tainted.getAstNode() = arg |
24+
taint_string = tainted.getTaintKind().toString()
25+
) and
26+
has_taint = true
27+
) and
28+
if expected_taint = has_taint then test_res = "ok" else test_res = "failure"
29+
// if expected_taint = has_taint then test_res = "✓" else test_res = "✕"
30+
select arg.getLocation().toString(), call.getScope().(Function).getName(), arg.toString(),
31+
taint_string, test_res
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
def random_choice():
2+
return bool(GLOBAL_UNKOWN_VAR)
3+
4+
def is_safe(arg):
5+
return UNKNOWN_FUNC(arg)
6+
7+
def true_func():
8+
return True
9+
10+
def test_basic():
11+
s = TAINTED_STRING
12+
13+
if is_safe(s):
14+
ensure_not_tainted(s)
15+
else:
16+
ensure_tainted(s)
17+
18+
if not is_safe(s):
19+
ensure_tainted(s)
20+
else:
21+
ensure_not_tainted(s)
22+
23+
24+
def test_or():
25+
s = TAINTED_STRING
26+
27+
# x or y
28+
if is_safe(s) or random_choice():
29+
ensure_tainted(s) # might be tainted
30+
else:
31+
ensure_tainted(s) # must be tainted
32+
33+
# not (x or y)
34+
if not(is_safe(s) or random_choice()):
35+
ensure_tainted(s) # must be tainted
36+
else:
37+
ensure_tainted(s) # might be tainted
38+
39+
# not (x or y) == not x and not y [de Morgan's laws]
40+
if not is_safe(s) and not random_choice():
41+
ensure_tainted(s) # must be tainted
42+
else:
43+
ensure_tainted(s) # might be tainted
44+
45+
46+
def test_and():
47+
s = TAINTED_STRING
48+
49+
# x and y
50+
if is_safe(s) and random_choice():
51+
ensure_not_tainted(s) # must not be tainted
52+
else:
53+
ensure_tainted(s) # might be tainted
54+
55+
# not (x and y)
56+
if not(is_safe(s) and random_choice()):
57+
ensure_tainted(s) # might be tainted
58+
else:
59+
ensure_not_tainted(s)
60+
61+
# not (x and y) == not x or not y [de Morgan's laws]
62+
if not is_safe(s) or not random_choice():
63+
ensure_tainted(s) # might be tainted
64+
else:
65+
ensure_not_tainted(s)
66+
67+
68+
def test_tricky():
69+
s = TAINTED_STRING
70+
71+
x = is_safe(s)
72+
if x:
73+
ensure_not_tainted(s) # FP
74+
75+
s_ = s
76+
if is_safe(s):
77+
ensure_not_tainted(s_) # FP
78+
79+
def test_nesting_not():
80+
s = TAINTED_STRING
81+
82+
if not(not(is_safe(s))):
83+
ensure_not_tainted(s)
84+
else:
85+
ensure_tainted(s)
86+
87+
if not(not(not(is_safe(s)))):
88+
ensure_tainted(s)
89+
else:
90+
ensure_not_tainted(s)
91+
92+
# Adding `and True` makes the sanitizer trigger when it would otherwise not. See output in
93+
# SanitizedEdges.expected and compare with `test_nesting_not` and `test_basic`
94+
def test_nesting_not_with_and_true():
95+
s = TAINTED_STRING
96+
97+
if not(is_safe(s) and True):
98+
ensure_tainted(s)
99+
else:
100+
ensure_not_tainted(s)
101+
102+
if not(not(is_safe(s) and True)):
103+
ensure_not_tainted(s)
104+
else:
105+
ensure_tainted(s)
106+
107+
if not(not(not(is_safe(s) and True))):
108+
ensure_tainted(s)
109+
else:
110+
ensure_not_tainted(s)

0 commit comments

Comments
 (0)