Skip to content

Commit 03bd6cb

Browse files
committed
python: Allow optional result=OK
Also add a further test case
1 parent d42bb11 commit 03bd6cb

File tree

7 files changed

+53
-33
lines changed

7 files changed

+53
-33
lines changed

python/ql/test/experimental/dataflow/TestUtil/DataflowQueryTest.qll

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,24 +9,39 @@ class DataFlowQueryTest extends InlineExpectationsTest {
99
override string getARelevantTag() { result = "result" }
1010

1111
override predicate hasActualResult(Location location, string element, string tag, string value) {
12-
exists(DataFlow::Configuration cfg, DataFlow::Node fromNode, DataFlow::Node toNode |
13-
cfg.hasFlow(fromNode, toNode)
14-
|
15-
location = toNode.getLocation() and
12+
exists(DataFlow::Configuration cfg, DataFlow::Node sink | cfg.hasFlowTo(sink) |
13+
location = sink.getLocation() and
1614
tag = "result" and
1715
value = "BAD" and
18-
element = toNode.toString()
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()
1933
)
2034
}
2135
}
2236

2337
query predicate missingAnnotationOnSink(Location location, string error, string element) {
2438
error = "ERROR, you should add `# $ MISSING: result=BAD` or `result=OK` annotation" and
2539
exists(DataFlow::Node sink |
40+
exists(sink.getLocation().getFile().getRelativePath()) and
2641
exists(DataFlow::Configuration cfg | cfg.isSink(sink) or cfg.isSink(sink, _)) and
2742
location = sink.getLocation() and
2843
element = prettyExpr(sink.asExpr()) and
29-
not any(DataFlow::Configuration config).hasFlow(_, sink) and
44+
not exists(DataFlow::Configuration cfg | cfg.hasFlowTo(sink)) and
3045
not exists(FalseNegativeExpectation missingResult |
3146
missingResult.getTag() = "result" and
3247
missingResult.getValue() = "BAD" and
@@ -35,7 +50,7 @@ query predicate missingAnnotationOnSink(Location location, string error, string
3550
) and
3651
not exists(GoodExpectation okResult |
3752
okResult.getTag() = "result" and
38-
okResult.getValue() = "OK" and
53+
okResult.getValue() in ["OK", "OK(" + prettyNode(sink) + ")"] and
3954
okResult.getLocation().getFile() = location.getFile() and
4055
okResult.getLocation().getStartLine() = location.getStartLine()
4156
)
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

0 commit comments

Comments
 (0)