python: Allow optional result=OK

Also add a further test case
This commit is contained in:
Rasmus Lerchedahl Petersen
2023-01-06 11:21:35 +01:00
parent d42bb119fe
commit 03bd6cb414
7 changed files with 53 additions and 33 deletions

View File

@@ -9,13 +9,27 @@ class DataFlowQueryTest extends InlineExpectationsTest {
override string getARelevantTag() { result = "result" }
override predicate hasActualResult(Location location, string element, string tag, string value) {
exists(DataFlow::Configuration cfg, DataFlow::Node fromNode, DataFlow::Node toNode |
cfg.hasFlow(fromNode, toNode)
|
location = toNode.getLocation() and
exists(DataFlow::Configuration cfg, DataFlow::Node sink | cfg.hasFlowTo(sink) |
location = sink.getLocation() and
tag = "result" and
value = "BAD" and
element = toNode.toString()
element = sink.toString()
)
}
// We allow annotating any sink with `result=OK` to signal
// safe sinks.
// Sometimes a line contains both an alert and a safe sink.
// In this situation, the annotation form `OK(safe sink)`
// can be useful.
override predicate hasOptionalResult(Location location, string element, string tag, string value) {
exists(DataFlow::Configuration cfg, DataFlow::Node sink |
cfg.isSink(sink) or cfg.isSink(sink, _)
|
location = sink.getLocation() and
tag = "result" and
value in ["OK", "OK(" + prettyNode(sink) + ")"] and
element = sink.toString()
)
}
}
@@ -23,10 +37,11 @@ class DataFlowQueryTest extends InlineExpectationsTest {
query predicate missingAnnotationOnSink(Location location, string error, string element) {
error = "ERROR, you should add `# $ MISSING: result=BAD` or `result=OK` annotation" and
exists(DataFlow::Node sink |
exists(sink.getLocation().getFile().getRelativePath()) and
exists(DataFlow::Configuration cfg | cfg.isSink(sink) or cfg.isSink(sink, _)) and
location = sink.getLocation() and
element = prettyExpr(sink.asExpr()) and
not any(DataFlow::Configuration config).hasFlow(_, sink) and
not exists(DataFlow::Configuration cfg | cfg.hasFlowTo(sink)) and
not exists(FalseNegativeExpectation missingResult |
missingResult.getTag() = "result" and
missingResult.getValue() = "BAD" and
@@ -35,7 +50,7 @@ query predicate missingAnnotationOnSink(Location location, string error, string
) and
not exists(GoodExpectation okResult |
okResult.getTag() = "result" and
okResult.getValue() = "OK" and
okResult.getValue() in ["OK", "OK(" + prettyNode(sink) + ")"] and
okResult.getLocation().getFile() = location.getFile() and
okResult.getLocation().getStartLine() = location.getStartLine()
)

View File

@@ -0,0 +1,2 @@
missingAnnotationOnSink
failures

View File

@@ -0,0 +1,3 @@
import python
import experimental.dataflow.TestUtil.DataflowQueryTest
import semmle.python.security.dataflow.PathInjectionQuery

View File

@@ -10,7 +10,7 @@ STATIC_DIR = "/server/static/"
def download_file():
filename = request.args.get('filename', '')
# ok since `send_from_directory` ensure this stays within `STATIC_DIR`
return send_from_directory(STATIC_DIR, filename) # OK
return send_from_directory(STATIC_DIR, filename) # $result=OK
# see https://flask.palletsprojects.com/en/1.1.x/api/#flask.send_from_directory
@@ -18,4 +18,4 @@ def download_file():
def download_file():
dirname = request.args.get('dirname', '')
filename = request.args.get('filename', '')
return send_from_directory(dirname, filename) # NOT OK
return send_from_directory(dirname, filename) # $result=BAD result=OK(filename)

View File

@@ -10,7 +10,7 @@ STATIC_DIR = "/server/static/"
@app.route("/path1")
def path_injection():
filename = request.args.get('filename', '')
f = open(os.path.join(STATIC_DIR, filename)) # NOT OK
f = open(os.path.join(STATIC_DIR, filename)) # $result=BAD
@app.route("/path2")
@@ -18,7 +18,7 @@ def path_injection():
# Normalized, but not checked
filename = request.args.get('filename', '')
npath = os.path.normpath(os.path.join(STATIC_DIR, filename))
f = open(npath) # NOT OK
f = open(npath) # $result=BAD
@app.route("/path3")
@@ -28,7 +28,7 @@ def unsafe_path_normpath():
npath = os.path.normpath(os.path.join(STATIC_DIR, filename))
if npath.startswith(STATIC_DIR):
pass
f = open(npath) # NOT OK
f = open(npath) # $result=BAD
@app.route("/path4")
@@ -37,7 +37,7 @@ def safe_path_normpath():
filename = request.args.get('filename', '')
npath = os.path.normpath(os.path.join(STATIC_DIR, filename))
if npath.startswith(STATIC_DIR):
f = open(npath) # OK
f = open(npath) # $result=OK
@app.route("/path5")
@@ -45,7 +45,7 @@ def unsafe_path_realpath():
# Normalized (by `realpath` that also follows symlinks), but not checked properly
filename = request.args.get('filename', '')
npath = os.path.realpath(os.path.join(STATIC_DIR, filename))
f = open(npath) # NOT OK
f = open(npath) # $result=BAD
@app.route("/path6")
@@ -54,7 +54,7 @@ def safe_path_realpath():
filename = request.args.get('filename', '')
npath = os.path.realpath(os.path.join(STATIC_DIR, filename))
if npath.startswith(STATIC_DIR):
f = open(npath) # OK
f = open(npath) # $result=OK
@app.route("/path6")
@@ -62,7 +62,7 @@ def unsafe_path_abspath():
# Normalized (by `abspath`), but not checked properly
filename = request.args.get('filename', '')
npath = os.path.abspath(os.path.join(STATIC_DIR, filename))
f = open(npath) # NOT OK
f = open(npath) # $result=BAD
@app.route("/path7")
@@ -71,7 +71,7 @@ def safe_path_abspath():
filename = request.args.get('filename', '')
npath = os.path.abspath(os.path.join(STATIC_DIR, filename))
if npath.startswith(STATIC_DIR):
f = open(npath) # OK
f = open(npath) # $result=OK
@app.route("/abspath_tricky")
@@ -84,14 +84,14 @@ def safe_path_abspath_tricky():
filename = request.args.get('filename', '')
possibly_unsafe_path = os.path.join(STATIC_DIR, filename)
if os.path.abspath(possibly_unsafe_path).startswith(STATIC_DIR):
f = open(possibly_unsafe_path) # OK
f = open(possibly_unsafe_path) # $SPURIOUS: result=BAD
@app.route("/int-only/<int:foo_id>")
def flask_int_only(foo_id):
# This is OK, since the flask routing ensures that `foo_id` MUST be an integer.
path = os.path.join(STATIC_DIR, foo_id)
f = open(path) # OK TODO: FP
f = open(path) # $spurious: result=BAD
@app.route("/not-path/<foo>")
@@ -99,7 +99,7 @@ def flask_not_path(foo):
# On UNIX systems, this is OK, since without being marked as `<path:foo>`, flask
# routing ensures that `foo` cannot contain forward slashes (not by using %2F either).
path = os.path.join(STATIC_DIR, foo)
f = open(path) # OK if only running on UNIX systems, NOT OK if could be running on windows
f = open(path) # $result=BAD // OK if only running on UNIX systems, NOT OK if could be running on windows
@app.route("/no-dot-dot")
@@ -110,7 +110,7 @@ def no_dot_dot():
# handle if `filename` is an absolute path
if '../' in path:
return "not this time"
f = open(path) # NOT OK
f = open(path) # $result=BAD
@app.route("/no-dot-dot-with-prefix")
@@ -121,7 +121,7 @@ def no_dot_dot_with_prefix():
# Therefore, for UNIX-only programs, the `../` check is enough to stop path injections.
if '../' in path:
return "not this time"
f = open(path) # OK if only running on UNIX systems, NOT OK if could be running on windows
f = open(path) # $result=BAD // OK if only running on UNIX systems, NOT OK if could be running on windows
@app.route("/replace-slash")
@@ -129,7 +129,7 @@ def replace_slash():
filename = request.args.get('filename', '')
path = os.path.join(STATIC_DIR, filename)
sanitized = path.replace("/", "_")
f = open(sanitized) # OK if only running on UNIX systems, NOT OK if could be running on windows
f = open(sanitized) # $result=BAD // OK if only running on UNIX systems, NOT OK if could be running on windows
@app.route("/stackoverflow-solution")
@@ -139,7 +139,7 @@ def stackoverflow_solution():
path = os.path.join(STATIC_DIR, filename)
if os.path.commonprefix((os.path.realpath(path), STATIC_DIR)) != STATIC_DIR:
return "not this time"
f = open(path) # OK TODO: FP
f = open(path) # $SPURIOUS: result=BAD
SAFE_FILES = ['foo', 'bar', 'baz']
@@ -149,4 +149,4 @@ def safe_set_of_files():
filename = request.args.get('filename', '')
if filename in SAFE_FILES:
path = os.path.join(STATIC_DIR, filename)
f = open(path) # OK TODO: FP
f = open(path) # $SPURIOUS: result=BAD

View File

@@ -16,21 +16,21 @@ def normalize(x):
@app.route("/path")
def simple():
x = source()
open(x) # NOT OK
open(x) # $result=BAD
@app.route("/path")
def normalization():
x = source()
y = normalize(x)
open(y) # NOT OK
open(y) # $result=BAD
@app.route("/path")
def check():
x = source()
if x.startswith("subfolder/"):
open(x) # NOT OK
open(x) # $result=BAD
@app.route("/path")
@@ -38,7 +38,7 @@ def normalize_then_check():
x = source()
y = normalize(x)
if y.startswith("subfolder/"):
open(y) # OK
open(y) # $result=OK
@app.route("/path")
@@ -46,4 +46,4 @@ def check_then_normalize():
x = source()
if x.startswith("subfolder/"):
y = normalize(x)
open(y) # NOT OK
open(y) # $result=BAD

View File

@@ -20,7 +20,7 @@ def normalize_then_check():
x = source()
y = normalize(x) # <--- this call...
if y.startswith("subfolder/"):
open(y) # OK
open(y) # $result=OK
@app.route("/path")
@@ -29,7 +29,7 @@ def normalize_check_normalize():
y = normalize(x) # (...or this call...)
if y.startswith("subfolder/"):
z = normalize(y) # <--- ...can jump to here, resulting in FP
open(z) # OK
open(z) # $result=OK
# The problem does not manifest if we simply define an alias
@@ -42,4 +42,4 @@ def normalize_check_normalize_alias():
y = normpath(x)
if y.startswith("subfolder/"):
z = normpath(y)
open(z) # OK
open(z) # $result=OK