Skip to content

Commit 006eaf3

Browse files
authored
Merge pull request #11088 from yoff/python/inline-query-tests
Python: Inline query tests
2 parents 1d4b2fd + 03bd6cb commit 006eaf3

File tree

10 files changed

+107
-39
lines changed

10 files changed

+107
-39
lines changed
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import python
2+
import semmle.python.dataflow.new.DataFlow
3+
import TestUtilities.InlineExpectationsTest
4+
private import semmle.python.dataflow.new.internal.PrintNode
5+
6+
class DataFlowQueryTest extends InlineExpectationsTest {
7+
DataFlowQueryTest() { this = "DataFlowQueryTest" }
8+
9+
override string getARelevantTag() { result = "result" }
10+
11+
override predicate hasActualResult(Location location, string element, string tag, string value) {
12+
exists(DataFlow::Configuration cfg, DataFlow::Node sink | cfg.hasFlowTo(sink) |
13+
location = sink.getLocation() and
14+
tag = "result" and
15+
value = "BAD" and
16+
element = sink.toString()
17+
)
18+
}
19+
20+
// We allow annotating any sink with `result=OK` to signal
21+
// safe sinks.
22+
// Sometimes a line contains both an alert and a safe sink.
23+
// In this situation, the annotation form `OK(safe sink)`
24+
// can be useful.
25+
override predicate hasOptionalResult(Location location, string element, string tag, string value) {
26+
exists(DataFlow::Configuration cfg, DataFlow::Node sink |
27+
cfg.isSink(sink) or cfg.isSink(sink, _)
28+
|
29+
location = sink.getLocation() and
30+
tag = "result" and
31+
value in ["OK", "OK(" + prettyNode(sink) + ")"] and
32+
element = sink.toString()
33+
)
34+
}
35+
}
36+
37+
query predicate missingAnnotationOnSink(Location location, string error, string element) {
38+
error = "ERROR, you should add `# $ MISSING: result=BAD` or `result=OK` annotation" and
39+
exists(DataFlow::Node sink |
40+
exists(sink.getLocation().getFile().getRelativePath()) and
41+
exists(DataFlow::Configuration cfg | cfg.isSink(sink) or cfg.isSink(sink, _)) and
42+
location = sink.getLocation() and
43+
element = prettyExpr(sink.asExpr()) and
44+
not exists(DataFlow::Configuration cfg | cfg.hasFlowTo(sink)) and
45+
not exists(FalseNegativeExpectation missingResult |
46+
missingResult.getTag() = "result" and
47+
missingResult.getValue() = "BAD" and
48+
missingResult.getLocation().getFile() = location.getFile() and
49+
missingResult.getLocation().getStartLine() = location.getStartLine()
50+
) and
51+
not exists(GoodExpectation okResult |
52+
okResult.getTag() = "result" and
53+
okResult.getValue() in ["OK", "OK(" + prettyNode(sink) + ")"] and
54+
okResult.getLocation().getFile() = location.getFile() and
55+
okResult.getLocation().getStartLine() = location.getStartLine()
56+
)
57+
)
58+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
missingAnnotationOnSink
2+
failures
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import python
2+
import experimental.dataflow.TestUtil.DataflowQueryTest
3+
import semmle.python.security.dataflow.PathInjectionQuery

python/ql/test/query-tests/Security/CWE-022-PathInjection/flask_path_injection.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,12 @@
1010
def download_file():
1111
filename = request.args.get('filename', '')
1212
# ok since `send_from_directory` ensure this stays within `STATIC_DIR`
13-
return send_from_directory(STATIC_DIR, filename) # OK
13+
return send_from_directory(STATIC_DIR, filename) # $result=OK
1414

1515

1616
# see https://flask.palletsprojects.com/en/1.1.x/api/#flask.send_from_directory
1717
@app.route("/also-provide-dirname")
1818
def download_file():
1919
dirname = request.args.get('dirname', '')
2020
filename = request.args.get('filename', '')
21-
return send_from_directory(dirname, filename) # NOT OK
21+
return send_from_directory(dirname, filename) # $result=BAD result=OK(filename)

python/ql/test/query-tests/Security/CWE-022-PathInjection/path_injection.py

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,15 @@
1010
@app.route("/path1")
1111
def path_injection():
1212
filename = request.args.get('filename', '')
13-
f = open(os.path.join(STATIC_DIR, filename)) # NOT OK
13+
f = open(os.path.join(STATIC_DIR, filename)) # $result=BAD
1414

1515

1616
@app.route("/path2")
1717
def path_injection():
1818
# Normalized, but not checked
1919
filename = request.args.get('filename', '')
2020
npath = os.path.normpath(os.path.join(STATIC_DIR, filename))
21-
f = open(npath) # NOT OK
21+
f = open(npath) # $result=BAD
2222

2323

2424
@app.route("/path3")
@@ -28,7 +28,7 @@ def unsafe_path_normpath():
2828
npath = os.path.normpath(os.path.join(STATIC_DIR, filename))
2929
if npath.startswith(STATIC_DIR):
3030
pass
31-
f = open(npath) # NOT OK
31+
f = open(npath) # $result=BAD
3232

3333

3434
@app.route("/path4")
@@ -37,15 +37,15 @@ def safe_path_normpath():
3737
filename = request.args.get('filename', '')
3838
npath = os.path.normpath(os.path.join(STATIC_DIR, filename))
3939
if npath.startswith(STATIC_DIR):
40-
f = open(npath) # OK
40+
f = open(npath) # $result=OK
4141

4242

4343
@app.route("/path5")
4444
def unsafe_path_realpath():
4545
# Normalized (by `realpath` that also follows symlinks), but not checked properly
4646
filename = request.args.get('filename', '')
4747
npath = os.path.realpath(os.path.join(STATIC_DIR, filename))
48-
f = open(npath) # NOT OK
48+
f = open(npath) # $result=BAD
4949

5050

5151
@app.route("/path6")
@@ -54,15 +54,15 @@ def safe_path_realpath():
5454
filename = request.args.get('filename', '')
5555
npath = os.path.realpath(os.path.join(STATIC_DIR, filename))
5656
if npath.startswith(STATIC_DIR):
57-
f = open(npath) # OK
57+
f = open(npath) # $result=OK
5858

5959

6060
@app.route("/path6")
6161
def unsafe_path_abspath():
6262
# Normalized (by `abspath`), but not checked properly
6363
filename = request.args.get('filename', '')
6464
npath = os.path.abspath(os.path.join(STATIC_DIR, filename))
65-
f = open(npath) # NOT OK
65+
f = open(npath) # $result=BAD
6666

6767

6868
@app.route("/path7")
@@ -71,7 +71,7 @@ def safe_path_abspath():
7171
filename = request.args.get('filename', '')
7272
npath = os.path.abspath(os.path.join(STATIC_DIR, filename))
7373
if npath.startswith(STATIC_DIR):
74-
f = open(npath) # OK
74+
f = open(npath) # $result=OK
7575

7676

7777
@app.route("/abspath_tricky")
@@ -84,22 +84,22 @@ def safe_path_abspath_tricky():
8484
filename = request.args.get('filename', '')
8585
possibly_unsafe_path = os.path.join(STATIC_DIR, filename)
8686
if os.path.abspath(possibly_unsafe_path).startswith(STATIC_DIR):
87-
f = open(possibly_unsafe_path) # OK
87+
f = open(possibly_unsafe_path) # $SPURIOUS: result=BAD
8888

8989

9090
@app.route("/int-only/<int:foo_id>")
9191
def flask_int_only(foo_id):
9292
# This is OK, since the flask routing ensures that `foo_id` MUST be an integer.
9393
path = os.path.join(STATIC_DIR, foo_id)
94-
f = open(path) # OK TODO: FP
94+
f = open(path) # $spurious: result=BAD
9595

9696

9797
@app.route("/not-path/<foo>")
9898
def flask_not_path(foo):
9999
# On UNIX systems, this is OK, since without being marked as `<path:foo>`, flask
100100
# routing ensures that `foo` cannot contain forward slashes (not by using %2F either).
101101
path = os.path.join(STATIC_DIR, foo)
102-
f = open(path) # OK if only running on UNIX systems, NOT OK if could be running on windows
102+
f = open(path) # $result=BAD // OK if only running on UNIX systems, NOT OK if could be running on windows
103103

104104

105105
@app.route("/no-dot-dot")
@@ -110,7 +110,7 @@ def no_dot_dot():
110110
# handle if `filename` is an absolute path
111111
if '../' in path:
112112
return "not this time"
113-
f = open(path) # NOT OK
113+
f = open(path) # $result=BAD
114114

115115

116116
@app.route("/no-dot-dot-with-prefix")
@@ -121,15 +121,15 @@ def no_dot_dot_with_prefix():
121121
# Therefore, for UNIX-only programs, the `../` check is enough to stop path injections.
122122
if '../' in path:
123123
return "not this time"
124-
f = open(path) # OK if only running on UNIX systems, NOT OK if could be running on windows
124+
f = open(path) # $result=BAD // OK if only running on UNIX systems, NOT OK if could be running on windows
125125

126126

127127
@app.route("/replace-slash")
128128
def replace_slash():
129129
filename = request.args.get('filename', '')
130130
path = os.path.join(STATIC_DIR, filename)
131131
sanitized = path.replace("/", "_")
132-
f = open(sanitized) # OK if only running on UNIX systems, NOT OK if could be running on windows
132+
f = open(sanitized) # $result=BAD // OK if only running on UNIX systems, NOT OK if could be running on windows
133133

134134

135135
@app.route("/stackoverflow-solution")
@@ -139,7 +139,7 @@ def stackoverflow_solution():
139139
path = os.path.join(STATIC_DIR, filename)
140140
if os.path.commonprefix((os.path.realpath(path), STATIC_DIR)) != STATIC_DIR:
141141
return "not this time"
142-
f = open(path) # OK TODO: FP
142+
f = open(path) # $SPURIOUS: result=BAD
143143

144144

145145
SAFE_FILES = ['foo', 'bar', 'baz']
@@ -149,4 +149,4 @@ def safe_set_of_files():
149149
filename = request.args.get('filename', '')
150150
if filename in SAFE_FILES:
151151
path = os.path.join(STATIC_DIR, filename)
152-
f = open(path) # OK TODO: FP
152+
f = open(path) # $SPURIOUS: result=BAD

python/ql/test/query-tests/Security/CWE-022-PathInjection/test.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,34 +16,34 @@ def normalize(x):
1616
@app.route("/path")
1717
def simple():
1818
x = source()
19-
open(x) # NOT OK
19+
open(x) # $result=BAD
2020

2121

2222
@app.route("/path")
2323
def normalization():
2424
x = source()
2525
y = normalize(x)
26-
open(y) # NOT OK
26+
open(y) # $result=BAD
2727

2828

2929
@app.route("/path")
3030
def check():
3131
x = source()
3232
if x.startswith("subfolder/"):
33-
open(x) # NOT OK
33+
open(x) # $result=BAD
3434

3535

3636
@app.route("/path")
3737
def normalize_then_check():
3838
x = source()
3939
y = normalize(x)
4040
if y.startswith("subfolder/"):
41-
open(y) # OK
41+
open(y) # $result=OK
4242

4343

4444
@app.route("/path")
4545
def check_then_normalize():
4646
x = source()
4747
if x.startswith("subfolder/"):
4848
y = normalize(x)
49-
open(y) # NOT OK
49+
open(y) # $result=BAD

python/ql/test/query-tests/Security/CWE-022-PathInjection/test_chaining.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ def normalize_then_check():
2020
x = source()
2121
y = normalize(x) # <--- this call...
2222
if y.startswith("subfolder/"):
23-
open(y) # OK
23+
open(y) # $result=OK
2424

2525

2626
@app.route("/path")
@@ -29,7 +29,7 @@ def normalize_check_normalize():
2929
y = normalize(x) # (...or this call...)
3030
if y.startswith("subfolder/"):
3131
z = normalize(y) # <--- ...can jump to here, resulting in FP
32-
open(z) # OK
32+
open(z) # $result=OK
3333

3434

3535
# The problem does not manifest if we simply define an alias
@@ -42,4 +42,4 @@ def normalize_check_normalize_alias():
4242
y = normpath(x)
4343
if y.startswith("subfolder/"):
4444
z = normpath(y)
45-
open(z) # OK
45+
open(z) # $result=OK
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
missingAnnotationOnSink
2+
failures
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import python
2+
import experimental.dataflow.TestUtil.DataflowQueryTest
3+
import semmle.python.security.dataflow.CommandInjectionQuery

python/ql/test/query-tests/Security/CWE-078-CommandInjection/command_injection.py

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,36 +10,36 @@
1010
def command_injection1():
1111
files = request.args.get('files', '')
1212
# Don't let files be `; rm -rf /`
13-
os.system("ls " + files)
13+
os.system("ls " + files) # $result=BAD
1414

1515

1616
@app.route("/command2")
1717
def command_injection2():
1818
files = request.args.get('files', '')
1919
# Don't let files be `; rm -rf /`
20-
subprocess.Popen("ls " + files, shell=True)
20+
subprocess.Popen("ls " + files, shell=True) # $result=BAD
2121

2222

2323
@app.route("/command3")
2424
def first_arg_injection():
2525
cmd = request.args.get('cmd', '')
26-
subprocess.Popen([cmd, "param1"])
26+
subprocess.Popen([cmd, "param1"]) # $result=BAD
2727

2828

2929
@app.route("/other_cases")
3030
def others():
3131
files = request.args.get('files', '')
3232
# Don't let files be `; rm -rf /`
33-
os.popen("ls " + files)
33+
os.popen("ls " + files) # $result=BAD
3434

3535

3636
@app.route("/multiple")
3737
def multiple():
3838
command = request.args.get('command', '')
3939
# We should mark flow to both calls here, which conflicts with removing flow out of
4040
# a sink due to use-use flow.
41-
os.system(command)
42-
os.system(command)
41+
os.system(command) # $result=BAD
42+
os.system(command) # $result=BAD
4343

4444

4545
@app.route("/not-into-sink-impl")
@@ -52,11 +52,11 @@ def not_into_sink_impl():
5252
subprocess.call implementation: https://github.com/python/cpython/blob/fa7ce080175f65d678a7d5756c94f82887fc9803/Lib/subprocess.py#L341
5353
"""
5454
command = request.args.get('command', '')
55-
os.system(command)
56-
os.popen(command)
57-
subprocess.call(command)
58-
subprocess.check_call(command)
59-
subprocess.run(command)
55+
os.system(command) # $result=BAD
56+
os.popen(command) # $result=BAD
57+
subprocess.call(command) # $result=BAD
58+
subprocess.check_call(command) # $result=BAD
59+
subprocess.run(command) # $result=BAD
6060

6161

6262
@app.route("/path-exists-not-sanitizer")
@@ -70,11 +70,11 @@ def path_exists_not_sanitizer():
7070
"""
7171
path = request.args.get('path', '')
7272
if os.path.exists(path):
73-
os.system("ls " + path) # NOT OK
73+
os.system("ls " + path) # $result=BAD
7474

7575

7676
@app.route("/restricted-characters")
7777
def restricted_characters():
7878
path = request.args.get('path', '')
7979
if re.match(r'^[a-zA-Z0-9_-]+$', path):
80-
os.system("ls " + path) # OK (TODO: Currently FP)
80+
os.system("ls " + path) # $SPURIOUS: result=BAD

0 commit comments

Comments
 (0)