From 54eb775cc958d5051746ad5d8868a41c3e7b6533 Mon Sep 17 00:00:00 2001 From: haby0 Date: Thu, 2 Dec 2021 18:51:30 +0800 Subject: [PATCH] Arbitrary file read and delete --- .../lib/semmle/python/frameworks/Django.qll | 43 ++++ .../lib/semmle/python/frameworks/FastApi.qll | 70 ++++++ .../ql/lib/semmle/python/frameworks/Flask.qll | 56 +++++ .../lib/semmle/python/frameworks/Stdlib.qll | 20 ++ .../lib/semmle/python/frameworks/Tornado.qll | 28 +++ .../CWE-073/ArbitraryFileReadAndDelete.py | 114 +++++++++ .../CWE-073/ArbitraryFileReadAndDelete.qhelp | 44 ++++ .../CWE-073/ArbitraryFileReadAndDelete.ql | 20 ++ .../CWE-073/ArbitraryFileReadAndDeleteLib.qll | 81 ++++++ .../experimental/semmle/python/Concepts.qll | 70 ++++++ .../experimental/semmle/python/Frameworks.qll | 1 + .../semmle/python/frameworks/File.qll | 231 ++++++++++++++++++ .../ArbitraryFileReadAndDelete.expected | 64 +++++ .../CWE-073/ArbitraryFileReadAndDelete.py | 114 +++++++++ .../CWE-073/ArbitraryFileReadAndDelete.qlref | 1 + 15 files changed, 957 insertions(+) create mode 100644 python/ql/src/experimental/Security/CWE-073/ArbitraryFileReadAndDelete.py create mode 100644 python/ql/src/experimental/Security/CWE-073/ArbitraryFileReadAndDelete.qhelp create mode 100644 python/ql/src/experimental/Security/CWE-073/ArbitraryFileReadAndDelete.ql create mode 100644 python/ql/src/experimental/Security/CWE-073/ArbitraryFileReadAndDeleteLib.qll create mode 100644 python/ql/src/experimental/semmle/python/frameworks/File.qll create mode 100644 python/ql/test/experimental/query-tests/Security/CWE-073/ArbitraryFileReadAndDelete.expected create mode 100644 python/ql/test/experimental/query-tests/Security/CWE-073/ArbitraryFileReadAndDelete.py create mode 100644 python/ql/test/experimental/query-tests/Security/CWE-073/ArbitraryFileReadAndDelete.qlref diff --git a/python/ql/lib/semmle/python/frameworks/Django.qll b/python/ql/lib/semmle/python/frameworks/Django.qll index 9e66c728f6ee..967f3639285a 100644 --- a/python/ql/lib/semmle/python/frameworks/Django.qll +++ b/python/ql/lib/semmle/python/frameworks/Django.qll @@ -1791,6 +1791,49 @@ module PrivateDjango { * See https://docs.djangoproject.com/en/3.1/topics/http/shortcuts/#redirect */ API::Node redirect() { result = shortcuts().getMember("redirect") } + + /** + * A call to the `django.shortcuts.render` function. + * + * See https://docs.djangoproject.com/en/3.1/topics/http/shortcuts/#render + */ + class ShortcutsRenderCall extends HTTP::Server::HttpResponse::Range, DataFlow::CallCfgNode { + ShortcutsRenderCall() { + this = API::moduleImport("django").getMember("shortcuts").getMember("render").getACall() + } + + override DataFlow::Node getBody() { + result in [this.getArg(2), this.getArgByName("context")] + } + + override DataFlow::Node getMimetypeOrContentTypeArg() { none() } + + override string getMimetypeDefault() { result = "text/html" } + } + + /** + * A call to the `django.shortcuts.render_to_response` function. + * + * See https://docs.djangoproject.com/en/2.2/topics/http/shortcuts/#render_to_response + */ + class ShortcutsRenderToResponseCall extends HTTP::Server::HttpResponse::Range, + DataFlow::CallCfgNode { + ShortcutsRenderToResponseCall() { + this = + API::moduleImport("django") + .getMember("shortcuts") + .getMember("render_to_response") + .getACall() + } + + override DataFlow::Node getBody() { + result in [this.getArg(1), this.getArgByName("context")] + } + + override DataFlow::Node getMimetypeOrContentTypeArg() { none() } + + override string getMimetypeDefault() { result = "text/html" } + } } } diff --git a/python/ql/lib/semmle/python/frameworks/FastApi.qll b/python/ql/lib/semmle/python/frameworks/FastApi.qll index 35ffdc43dd8e..db454bce1dc3 100644 --- a/python/ql/lib/semmle/python/frameworks/FastApi.qll +++ b/python/ql/lib/semmle/python/frameworks/FastApi.qll @@ -11,6 +11,7 @@ private import semmle.python.Concepts private import semmle.python.ApiGraphs private import semmle.python.frameworks.Pydantic private import semmle.python.frameworks.Starlette +private import semmle.python.frameworks.internal.InstanceTaintStepsHelper /** * Provides models for the `fastapi` PyPI package. @@ -55,6 +56,14 @@ private module FastApi { this = App::instance().getMember(routeAddingMethod).getACall() or this = APIRouter::instance().getMember(routeAddingMethod).getACall() + or + exists(DataFlow::AttrRead read, CallNode callNode, ClassValue cv | + cv.getASuperType() = Value::named("fastapi.APIRouter") and + callNode.getFunction().pointsTo(cv) and + read.(DataFlow::AttrRead).getObject().asCfgNode().refersTo(callNode) and + read.(DataFlow::AttrRead).getAttributeName() = routeAddingMethod and + this.getFunction() = read + ) ) } @@ -349,4 +358,65 @@ private module FastApi { override DataFlow::Node getValueArg() { none() } } } + + // --------------------------------------------------------------------------- + // request modeling + // --------------------------------------------------------------------------- + /** + * Provides models for the `fastapi.Request` class. + * + * See https://fastapi.tiangolo.com/advanced/using-request-directly + */ + module Request { + /** + * A source of instances of `fastapi.Request`, extend this class to model new instances. + * + * This can include instantiations of the class, return values from function + * calls, or a special parameter that will be set when functions are called by an external + * library. + * + * Use `Request::instance()` predicate to get + * references to instances of `fastapi.Request`. + */ + abstract class InstanceSource extends DataFlow::LocalSourceNode { } + + /** Gets a reference to an instance of `twisted.web.server.Request`. */ + private DataFlow::TypeTrackingNode instance(DataFlow::TypeTracker t) { + t.start() and + result instanceof InstanceSource + or + exists(DataFlow::TypeTracker t2 | result = instance(t2).track(t2, t)) + } + + /** Gets a reference to an instance of `fastapi.Request`. */ + DataFlow::Node instance() { instance(DataFlow::TypeTracker::end()).flowsTo(result) } + + /** + * Taint propagation for `fastapi.Request`. + */ + private class InstanceTaintSteps extends InstanceTaintStepsHelper { + InstanceTaintSteps() { this = "fastapi.Request" } + + override DataFlow::Node getInstance() { result = instance() } + + override string getAttributeName() { result in ["query_params", "headers", "cookies"] } + + override string getMethodName() { none() } + + override string getAsyncMethodName() { none() } + } + } + + /** + * A parameter that will receive a `fastapi.Request` instance, + * when a twisted request handler is called. + */ + class FastAPIRequestHandlerRequestParam extends RemoteFlowSource::Range, Request::InstanceSource, + DataFlow::ParameterNode { + FastAPIRequestHandlerRequestParam() { + this.getParameter() = any(FastApiRouteSetup handler).getARoutedParameter() + } + + override string getSourceType() { result = "fastapi.Request" } + } } diff --git a/python/ql/lib/semmle/python/frameworks/Flask.qll b/python/ql/lib/semmle/python/frameworks/Flask.qll index 88544ca85378..1948783bf954 100644 --- a/python/ql/lib/semmle/python/frameworks/Flask.qll +++ b/python/ql/lib/semmle/python/frameworks/Flask.qll @@ -180,6 +180,62 @@ module Flask { DataFlow::Node instance() { instance(DataFlow::TypeTracker::end()).flowsTo(result) } } + /** + * A call to the `flask.jsonify` or `flask.json.jsonify` function. + * + * See https://flask.palletsprojects.com/en/2.0.x/api/#flask.json.jsonify + */ + private class FlaskJsonifyCall extends HTTP::Server::HttpResponse::Range, DataFlow::CallCfgNode { + FlaskJsonifyCall() { + this = API::moduleImport("flask").getMember("jsonify").getACall() or + this = API::moduleImport("flask").getMember("json").getMember("jsonify").getACall() + } + + override DataFlow::Node getBody() { result = this.getArg(_) } + + override string getMimetypeDefault() { result = "application/json" } + + override DataFlow::Node getMimetypeOrContentTypeArg() { none() } + } + + /** + * A call to the `flask.render_template` function. + * + * See https://flask.palletsprojects.com/en/2.0.x/api/#flask.render_template + */ + private class FlaskRenderTemplateCall extends HTTP::Server::HttpResponse::Range, + DataFlow::CallCfgNode { + FlaskRenderTemplateCall() { + this = API::moduleImport("flask").getMember("render_template").getACall() + } + + override DataFlow::Node getBody() { + result = this.getArgByName(any(string s | s != "template_name_or_list")) + } + + override string getMimetypeDefault() { result = "text/html" } + + override DataFlow::Node getMimetypeOrContentTypeArg() { none() } + } + + /** + * A call to the `flask.render_template_string` function. + * + * See https://flask.palletsprojects.com/en/2.0.x/api/#flask.render_template_string + */ + private class FlaskRenderTemplateStringCall extends HTTP::Server::HttpResponse::Range, + DataFlow::CallCfgNode { + FlaskRenderTemplateStringCall() { + this = API::moduleImport("flask").getMember("render_template_string").getACall() + } + + override DataFlow::Node getBody() { result = this.getArgByName(_) } + + override string getMimetypeDefault() { result = "text/html" } + + override DataFlow::Node getMimetypeOrContentTypeArg() { none() } + } + // --------------------------------------------------------------------------- // routing modeling // --------------------------------------------------------------------------- diff --git a/python/ql/lib/semmle/python/frameworks/Stdlib.qll b/python/ql/lib/semmle/python/frameworks/Stdlib.qll index d7d3c6b44bc9..54cdafb69fbf 100644 --- a/python/ql/lib/semmle/python/frameworks/Stdlib.qll +++ b/python/ql/lib/semmle/python/frameworks/Stdlib.qll @@ -1320,6 +1320,26 @@ private module StdlibPrivate { } } + // --------------------------------------------------------------------------- + // Implicit response from returns of http request handlers + // --------------------------------------------------------------------------- + private class HTTPRequestHandlerResponse extends HTTP::Server::HttpResponse::Range, + DataFlow::CallCfgNode { + HTTPRequestHandlerResponse() { + exists(DataFlow::AttrRead read | + this.getFunction().(DataFlow::AttrRead).getObject() = read and + read.getObject() = instance() and + read.getAttributeName() = "wfile" + ) + } + + override DataFlow::Node getBody() { result = this.getArg(_) } + + override DataFlow::Node getMimetypeOrContentTypeArg() { none() } + + override string getMimetypeDefault() { result = "application/json" } + } + /** An `HTTPMessage` instance that originates from a `BaseHTTPRequestHandler` instance. */ private class BaseHTTPRequestHandlerHeadersInstances extends Stdlib::HTTPMessage::InstanceSource { BaseHTTPRequestHandlerHeadersInstances() { diff --git a/python/ql/lib/semmle/python/frameworks/Tornado.qll b/python/ql/lib/semmle/python/frameworks/Tornado.qll index 91ae3ac25753..e851120fd96d 100644 --- a/python/ql/lib/semmle/python/frameworks/Tornado.qll +++ b/python/ql/lib/semmle/python/frameworks/Tornado.qll @@ -167,6 +167,17 @@ private module Tornado { /** Gets a reference to the `write` method. */ DataFlow::Node writeMethod() { writeMethod(DataFlow::TypeTracker::end()).flowsTo(result) } + /** Gets a reference to the `render` or `render_string` method. */ + private DataFlow::TypeTrackingNode renderMethod(DataFlow::TypeTracker t) { + t.startInAttr(["render", "render_string"]) and + result = instance() + or + exists(DataFlow::TypeTracker t2 | result = renderMethod(t2).track(t2, t)) + } + + /** Gets a reference to the `render` or `render_string` method. */ + DataFlow::Node renderMethod() { renderMethod(DataFlow::TypeTracker::end()).flowsTo(result) } + /** * Taint propagation for `tornado.web.RequestHandler`. */ @@ -494,6 +505,23 @@ private module Tornado { override DataFlow::Node getMimetypeOrContentTypeArg() { none() } } + /** + * A call to the `tornado.web.RequestHandler.render` or `tornado.web.RequestHandler.render_string` method. + * + * See https://www.tornadoweb.org/en/stable/web.html#tornado.web.RequestHandler.render + */ + private class TornadoRenderCall extends HTTP::Server::HttpResponse::Range, DataFlow::CallCfgNode { + TornadoRenderCall() { this.getFunction() = tornado::web::RequestHandler::renderMethod() } + + override DataFlow::Node getBody() { + result = this.getArgByName(any(string s | s != "template_name")) + } + + override string getMimetypeDefault() { result = "text/html" } + + override DataFlow::Node getMimetypeOrContentTypeArg() { none() } + } + /** * A call to the `tornado.web.RequestHandler.set_cookie` method. * diff --git a/python/ql/src/experimental/Security/CWE-073/ArbitraryFileReadAndDelete.py b/python/ql/src/experimental/Security/CWE-073/ArbitraryFileReadAndDelete.py new file mode 100644 index 000000000000..7da8d00c5bee --- /dev/null +++ b/python/ql/src/experimental/Security/CWE-073/ArbitraryFileReadAndDelete.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +""" +@Desc : +""" +from fastapi import FastAPI, Request +from flask import Flask, request, send_file, make_response, helpers, jsonify, render_template,render_template_string, Response +from flask.json import jsonify +from pathlib import Path +from werkzeug.utils import secure_filename +import io +import os +import builtins + +app = Flask(__name__) + + +@app.route('/fileRead1') +def fileRead1(): + filename = request.args.get('filename') + context = io.open(filename) + response = make_response(context) + return response + +@app.route('/fileRead2') +def fileRead2(): + filename = request.args.get('filename') + context = open(filename, encoding="utf-8").read() + return context + +@app.route('/fileRead3') +def fileRead3(): + filename = request.args.get('filename') + context = "" + with builtins.open(filename, encoding="utf-8") as f: + context = f.readlines() + return context + +@app.route('/fileRead4') +def fileRead4(): + filename = request.args.get('filename') + response = send_file(path_or_file = "/home/work/" + filename) + return response + +@app.route('/fileRead5') +def fileRead5(): + filename = request.args.get('filename') + context = helpers.send_file(filename) + return context + +@app.route('/fileRead6') +def fileRead6(): + filename = request.args.get('filename') + context = os.listdir(filename) + return render_template_string("filedelete.html", contents = context) + +@app.route('/fileRead7') +def fileRead7(): + filename = request.args.get('filename') + fd = os.open(filename, os.O_RDWR|os.O_CREAT) + result = os.fdopen(fd).read() + return jsonify(result) + +@app.route('/fileRead8') +def fileRead8(): + filename = request.args.get('filename') + result = "" + for dir in os.scandir(filename): + result += str(dir) + return render_template_string(filename) + +@app.route('/fileDelete1') +def fileDelete1(): + filename = request.args.get('filename') + result = "" + try: + result = os.remove(filename) + except Exception as e: + result = "failed" + response = make_response(result) + return response + +@app.route('/fileDelete2') +def fileDelete2(): + filename = request.args.get('filename') + result = "" + try: + result = os.removedirs(filename) + except Exception as e: + result = "failed" + return result, 200 + +@app.route('/fileDelete3') +def fileDelete3(): + filename = request.args.get('filename') + result = "" + try: + result = os.unlink(filename) + except Exception as e: + result = "failed" + return render_template(template_name_or_list = "filedelete.html", contents = filename) + +@app.route('/good1') +def good1(): + filename = request.args.get('filename') + sec_filename = secure_filename(filename) + try: + result = os.unlink(sec_filename) + except Exception as e: + result = "failed" + +if __name__ == '__main__': + app.debug = True + app.run(host = '0.0.0.0') \ No newline at end of file diff --git a/python/ql/src/experimental/Security/CWE-073/ArbitraryFileReadAndDelete.qhelp b/python/ql/src/experimental/Security/CWE-073/ArbitraryFileReadAndDelete.qhelp new file mode 100644 index 000000000000..05ca4a8a0f01 --- /dev/null +++ b/python/ql/src/experimental/Security/CWE-073/ArbitraryFileReadAndDelete.qhelp @@ -0,0 +1,44 @@ + + + + +

+Accessing files using paths constructed by user-controlled data may allow attackers +to access unexpected resources, leading to the disclosure of sensitive information or file deletion +

+
+ + +

+After the user completes the file path construction, the file path needs to be strictly verified before +reading the file content or deleting the file. +

+ +
+ + +

+In the eight examples of fileRead1...8, the accessed file name or directory name comes from the user's input without +any verification and is directly returned to the user, causing sensitive information to be leaked. For example: +Linux system, the file name can enter "../../../etc/passwd" to obtain the system user password file. +

+ +

+In the three examples of fileDelete1...3, the deleted file name and directory name come from the user's input without +any verification, resulting in arbitrary file deletion. +

+ +

+In the good1 example, the program uses werkzeug.utils.secure_filename to filter the file name and does +not cause any file deletion vulnerability. +

+ + +
+ + +
  • OWASP: Path Traversal.
  • +
    +
    \ No newline at end of file diff --git a/python/ql/src/experimental/Security/CWE-073/ArbitraryFileReadAndDelete.ql b/python/ql/src/experimental/Security/CWE-073/ArbitraryFileReadAndDelete.ql new file mode 100644 index 000000000000..1835b044ca48 --- /dev/null +++ b/python/ql/src/experimental/Security/CWE-073/ArbitraryFileReadAndDelete.ql @@ -0,0 +1,20 @@ +/** + * @name Arbitrary File Read and Delete + * @description Accessing files using paths constructed by user-controlled data may allow attackers + * to access unexpected resources, leading to the disclosure of sensitive information or file deletion + * @kind path-problem + * @problem.severity error + * @precision high + * @id py/any-file-read-and-delete + * @tags security + * external/cwe/cwe-073 + */ + +import python +import DataFlow::PathGraph +import ArbitraryFileReadAndDeleteLib + +from ArbitraryFileReadAndDeleteFlowConfig config, DataFlow::PathNode source, DataFlow::PathNode sink +where config.hasFlowPath(source, sink) +select sink.getNode(), source, sink, "Arbitrary file read and delete might include code from $@.", + source.getNode(), "this user input" diff --git a/python/ql/src/experimental/Security/CWE-073/ArbitraryFileReadAndDeleteLib.qll b/python/ql/src/experimental/Security/CWE-073/ArbitraryFileReadAndDeleteLib.qll new file mode 100644 index 000000000000..b5c5a10924ae --- /dev/null +++ b/python/ql/src/experimental/Security/CWE-073/ArbitraryFileReadAndDeleteLib.qll @@ -0,0 +1,81 @@ +import python +import DataFlow +import semmle.python.Concepts +import semmle.python.ApiGraphs +import semmle.python.dataflow.new.TaintTracking +import semmle.python.dataflow.new.TaintTracking2 +import semmle.python.dataflow.new.RemoteFlowSources +import experimental.semmle.python.Concepts + +/** + * A call to the `werkzeug.utils.secure_filename` function. + * + * See https://werkzeug.palletsprojects.com/en/2.0.x/utils/#werkzeug.utils.secure_filename + */ +private class WerkzeugUtilsSecureFilenameCall extends DataFlow::CallCfgNode { + WerkzeugUtilsSecureFilenameCall() { + this = API::moduleImport("werkzeug").getMember("utils").getMember("secure_filename").getACall() + } + + DataFlow::Node getArgument() { result in [this.getArg(0), this.getArgByName("filename")] } +} + +/** + * A taint-tracking configuration for tracking untrusted user input used in file reading and deletion. + */ +class ArbitraryFileReadAndDeleteFlowConfig extends TaintTracking::Configuration { + ArbitraryFileReadAndDeleteFlowConfig() { this = "ArbitraryFileReadAndDeleteFlowConfig" } + + override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource } + + override predicate isSink(DataFlow::Node sink) { + sink instanceof ArbitraryFileReadSink or + sink instanceof ArbitraryFileOrDirRemoveSink + } + + override predicate isSanitizer(DataFlow::Node node) { + node = any(WerkzeugUtilsSecureFilenameCall wusf).getArgument() + } +} + +/** A data flow sink for arbitrary file remove vulnerabilities. */ +private class ArbitraryFileOrDirRemoveSink extends DataFlow::Node { + ArbitraryFileOrDirRemoveSink() { this = any(FileRemove fr).getAPathArgument() } +} + +/** A data flow sink for arbitrary file read vulnerabilities. */ +private class ArbitraryFileReadSink extends DataFlow::Node { + ArbitraryFileReadSink() { + exists(FileOpen fo | + fo.getAPathArgument() = this and + any(FileContentResponseFlowConfig rfc).hasFlow(fo.getCall(), _) + ) + } +} + +/** + * A taint-tracking configuration for file content to http response. + */ +class FileContentResponseFlowConfig extends TaintTracking2::Configuration { + FileContentResponseFlowConfig() { this = "FileContentResponseFlowConfig" } + + override predicate isSource(DataFlow::Node source) { source = any(FileOpen fo).getCall() } + + override predicate isSink(DataFlow::Node sink) { + sink = any(HTTP::Server::HttpResponse hr).getBody() + } + + override predicate isAdditionalTaintStep(DataFlow::Node node1, DataFlow::Node node2) { + exists(DataFlow::CallCfgNode call | + call = API::moduleImport("os").getMember("fdopen").getACall() and + call.getArg(0) = node1 and + call = node2 + ) + or + exists(DataFlow::CallCfgNode ccn | + ccn.getFunction().(AttrRead).getAttributeName() in ["read_text", "read_bytes"] and + ccn.getFunction().(AttrRead).getObject() = node1 and + ccn = node2 + ) + } +} diff --git a/python/ql/src/experimental/semmle/python/Concepts.qll b/python/ql/src/experimental/semmle/python/Concepts.qll index 3b1f6072f0c2..4a0b5e6f7db7 100644 --- a/python/ql/src/experimental/semmle/python/Concepts.qll +++ b/python/ql/src/experimental/semmle/python/Concepts.qll @@ -14,6 +14,76 @@ private import semmle.python.dataflow.new.RemoteFlowSources private import semmle.python.dataflow.new.TaintTracking private import experimental.semmle.python.Frameworks +/** + * A data flow node that performs a file system access, including reading and writing data, + * creating and deleting files and folders, checking and updating permissions, and so on. + * + * Extend this class to refine existing API models. If you want to model new APIs, + * extend `FileSystemAccess::Range` instead. + */ +class FileOpen extends DataFlow::Node { + FileOpen::Range range; + + FileOpen() { this = range } + + /** Gets an argument to this file system access that is interpreted as a path. */ + DataFlow::Node getAPathArgument() { result = range.getAPathArgument() } + + DataFlow::CallCfgNode getCall() { result = range.getCall() } +} + +/** Provides a class for modeling new file system access APIs. */ +module FileOpen { + /** + * A data-flow node that performs a file system access, including reading and writing data, + * creating and deleting files and folders, checking and updating permissions, and so on. + * + * Extend this class to model new APIs. If you want to refine existing API models, + * extend `FileSystemAccess` instead. + */ + abstract class Range extends DataFlow::Node { + /** Gets an argument to this file system access that is interpreted as a path. */ + abstract DataFlow::Node getAPathArgument(); + + abstract DataFlow::CallCfgNode getCall(); + } +} + +/** + * A data flow node that performs a file system access, including reading and writing data, + * creating and deleting files and folders, checking and updating permissions, and so on. + * + * Extend this class to refine existing API models. If you want to model new APIs, + * extend `FileSystemAccess::Range` instead. + */ +class FileRemove extends DataFlow::Node { + FileRemove::Range range; + + FileRemove() { this = range } + + /** Gets an argument to this file system access that is interpreted as a path. */ + DataFlow::Node getAPathArgument() { result = range.getAPathArgument() } + + DataFlow::CallCfgNode getCall() { result = range.getCall() } +} + +/** Provides a class for modeling new file system access APIs. */ +module FileRemove { + /** + * A data-flow node that performs a file system access, including reading and writing data, + * creating and deleting files and folders, checking and updating permissions, and so on. + * + * Extend this class to model new APIs. If you want to refine existing API models, + * extend `FileSystemAccess` instead. + */ + abstract class Range extends DataFlow::Node { + /** Gets an argument to this file system access that is interpreted as a path. */ + abstract DataFlow::Node getAPathArgument(); + + abstract DataFlow::CallCfgNode getCall(); + } +} + /** Provides classes for modeling log related APIs. */ module LogOutput { /** diff --git a/python/ql/src/experimental/semmle/python/Frameworks.qll b/python/ql/src/experimental/semmle/python/Frameworks.qll index 90fe21cc9331..680e63d5eca9 100644 --- a/python/ql/src/experimental/semmle/python/Frameworks.qll +++ b/python/ql/src/experimental/semmle/python/Frameworks.qll @@ -13,3 +13,4 @@ private import experimental.semmle.python.frameworks.JWT private import experimental.semmle.python.libraries.PyJWT private import experimental.semmle.python.libraries.Authlib private import experimental.semmle.python.libraries.PythonJose +private import experimental.semmle.python.frameworks.File diff --git a/python/ql/src/experimental/semmle/python/frameworks/File.qll b/python/ql/src/experimental/semmle/python/frameworks/File.qll new file mode 100644 index 000000000000..483ba53c855a --- /dev/null +++ b/python/ql/src/experimental/semmle/python/frameworks/File.qll @@ -0,0 +1,231 @@ +/** + * Provides classes modeling security-relevant aspects of the file system access libraries. + */ + +private import python +private import semmle.python.dataflow.new.DataFlow +private import semmle.python.dataflow.new.TaintTracking +private import semmle.python.dataflow.new.RemoteFlowSources +private import experimental.semmle.python.Concepts +private import semmle.python.ApiGraphs + +/** + * Provides models for Python's file-system libraries. + */ +private module File { + /** + * A call to the `builtins.open` function. + * + * See https://docs.python.org/3/library/functions.html#open + */ + private class BuiltinOpenCall extends DataFlow::CallCfgNode, FileOpen::Range { + BuiltinOpenCall() { this = API::builtin("open").getACall() } + + override DataFlow::Node getAPathArgument() { + result in [this.getArg(0), this.getArgByName("file")] + } + + override DataFlow::CallCfgNode getCall() { result = this } + } + + /** + * A call to the `io.open` function. + * + * See https://docs.python.org/3/library/io.html#io.open + */ + private class IoOpenCall extends DataFlow::CallCfgNode, FileOpen::Range { + IoOpenCall() { this = API::moduleImport("io").getMember("open").getACall() } + + override DataFlow::Node getAPathArgument() { + result in [this.getArg(0), this.getArgByName("file")] + } + + override DataFlow::CallCfgNode getCall() { result = this } + } + + /** + * A call to the `pathlib.Path` function. + * + * See https://docs.python.org/3/library/pathlib.html#pathlib.Path + */ + private class PathlibPathCall extends DataFlow::CallCfgNode, FileOpen::Range { + PathlibPathCall() { this = API::moduleImport("pathlib").getMember("Path").getACall() } + + override DataFlow::Node getAPathArgument() { result = this.getArg(0) } + + override DataFlow::CallCfgNode getCall() { result = this } + } + + /** + * A call to the `os.open` function. + * + * See https://docs.python.org/3/library/os.html#os.open + */ + private class OsOpenCall extends DataFlow::CallCfgNode, FileOpen::Range { + OsOpenCall() { this = API::moduleImport("os").getMember("open").getACall() } + + override DataFlow::Node getAPathArgument() { + result in [this.getArg(0), this.getArgByName("path")] + } + + override DataFlow::CallCfgNode getCall() { result = this } + } + + /** + * A call to the `os.listdir` function. + * + * See https://docs.python.org/3/library/os.html#os.listdir + */ + private class OsListDirCall extends DataFlow::CallCfgNode, FileOpen::Range { + OsListDirCall() { this = API::moduleImport("os").getMember("listdir").getACall() } + + override DataFlow::Node getAPathArgument() { + result in [this.getArg(0), this.getArgByName("path")] + } + + override DataFlow::CallCfgNode getCall() { result = this } + } + + /** + * A call to the `os.scandir` function. + * + * See https://docs.python.org/3/library/os.html#os.scandir + */ + private class OsScanDirCall extends DataFlow::CallCfgNode, FileOpen::Range { + OsScanDirCall() { this = API::moduleImport("os").getMember("scandir").getACall() } + + override DataFlow::Node getAPathArgument() { + result in [this.getArg(0), this.getArgByName("path")] + } + + override DataFlow::CallCfgNode getCall() { result = this } + } + + /** + * A call to the `flask.send_file` function. + * + * See https://flask.palletsprojects.com/en/2.0.x/api/#flask.send_file + */ + private class FlaskSendFileCall extends DataFlow::CallCfgNode, FileOpen::Range { + FlaskSendFileCall() { + this = API::moduleImport("flask").getMember("send_file").getACall() or + this = API::moduleImport("flask").getMember("helpers").getMember("send_file").getACall() + } + + override DataFlow::Node getAPathArgument() { + result in [this.getArg(0), this.getArgByName(["filename_or_fp", "path_or_file"])] + } + + override DataFlow::CallCfgNode getCall() { result = this } + } + + /** + * A call to the `flask.send_from_directory` function. + * + * See https://flask.palletsprojects.com/en/2.0.x/api/#flask.send_from_directory + */ + private class FlaskSendFromDirectoryCall extends DataFlow::CallCfgNode, FileOpen::Range { + FlaskSendFromDirectoryCall() { + this = API::moduleImport("flask").getMember("send_from_directory").getACall() + } + + override DataFlow::Node getAPathArgument() { + result in [this.getArg(1), this.getArgByName(["filename", "path"])] + } + + override DataFlow::CallCfgNode getCall() { result = this } + } + + /** + * A call to the `fastapi.responses.FileResponse` function. + */ + private class FastAPIFileResponseCall extends DataFlow::CallCfgNode, FileOpen::Range { + FastAPIFileResponseCall() { + this = + API::moduleImport("fastapi").getMember("responses").getMember("FileResponse").getACall() or + this = + API::moduleImport("starlette").getMember("responses").getMember("FileResponse").getACall() + } + + override DataFlow::Node getAPathArgument() { + result in [this.getArg(0), this.getArgByName("path")] + } + + override DataFlow::CallCfgNode getCall() { result = this } + } + + /** + * A call to the `os.remove` function. + * + * See https://docs.python.org/3/library/os.html#os.remove + */ + private class OsRemoveCall extends DataFlow::CallCfgNode, FileRemove::Range { + OsRemoveCall() { this = API::moduleImport("os").getMember("remove").getACall() } + + override DataFlow::Node getAPathArgument() { + result in [this.getArg(0), this.getArgByName("path")] + } + + override DataFlow::CallCfgNode getCall() { result = this } + } + + /** + * A call to the `os.removedirs` function. + * + * See https://docs.python.org/3/library/os.html#os.removedirs + */ + private class OsRemoveDirsCall extends DataFlow::CallCfgNode, FileRemove::Range { + OsRemoveDirsCall() { this = API::moduleImport("os").getMember("removedirs").getACall() } + + override DataFlow::Node getAPathArgument() { + result in [this.getArg(0), this.getArgByName("name")] + } + + override DataFlow::CallCfgNode getCall() { result = this } + } + + /** + * A call to the `os.rmdir` function. + * + * See https://docs.python.org/3/library/os.html#os.rmdir + */ + private class OsRmdirCall extends DataFlow::CallCfgNode, FileRemove::Range { + OsRmdirCall() { this = API::moduleImport("os").getMember("rmdir").getACall() } + + override DataFlow::Node getAPathArgument() { + result in [this.getArg(0), this.getArgByName("path")] + } + + override DataFlow::CallCfgNode getCall() { result = this } + } + + /** + * A call to the `os.unlink` function. + * + * See https://docs.python.org/3/library/os.html#os.unlink + */ + private class OsUnlinkCall extends DataFlow::CallCfgNode, FileRemove::Range { + OsUnlinkCall() { this = API::moduleImport("os").getMember("unlink").getACall() } + + override DataFlow::Node getAPathArgument() { + result in [this.getArg(0), this.getArgByName("path")] + } + + override DataFlow::CallCfgNode getCall() { result = this } + } + + /** + * A call to the `shutil.rmtree` function. + * + * See https://docs.python.org/3/library/shutil.html#shutil.rmtree + */ + private class ShutilRmtreeCall extends DataFlow::CallCfgNode, FileRemove::Range { + ShutilRmtreeCall() { this = API::moduleImport("shutil").getMember("rmtree").getACall() } + + override DataFlow::Node getAPathArgument() { + result in [this.getArg(0), this.getArgByName("path")] + } + + override DataFlow::CallCfgNode getCall() { result = this } + } +} diff --git a/python/ql/test/experimental/query-tests/Security/CWE-073/ArbitraryFileReadAndDelete.expected b/python/ql/test/experimental/query-tests/Security/CWE-073/ArbitraryFileReadAndDelete.expected new file mode 100644 index 000000000000..d956900558b8 --- /dev/null +++ b/python/ql/test/experimental/query-tests/Security/CWE-073/ArbitraryFileReadAndDelete.expected @@ -0,0 +1,64 @@ +edges +| ArbitraryFileReadAndDelete.py:20:16:20:22 | ControlFlowNode for request | ArbitraryFileReadAndDelete.py:20:16:20:27 | ControlFlowNode for Attribute | +| ArbitraryFileReadAndDelete.py:20:16:20:27 | ControlFlowNode for Attribute | ArbitraryFileReadAndDelete.py:21:23:21:30 | ControlFlowNode for filename | +| ArbitraryFileReadAndDelete.py:27:16:27:22 | ControlFlowNode for request | ArbitraryFileReadAndDelete.py:27:16:27:27 | ControlFlowNode for Attribute | +| ArbitraryFileReadAndDelete.py:27:16:27:27 | ControlFlowNode for Attribute | ArbitraryFileReadAndDelete.py:28:20:28:27 | ControlFlowNode for filename | +| ArbitraryFileReadAndDelete.py:33:16:33:22 | ControlFlowNode for request | ArbitraryFileReadAndDelete.py:33:16:33:27 | ControlFlowNode for Attribute | +| ArbitraryFileReadAndDelete.py:33:16:33:27 | ControlFlowNode for Attribute | ArbitraryFileReadAndDelete.py:35:24:35:31 | ControlFlowNode for filename | +| ArbitraryFileReadAndDelete.py:41:16:41:22 | ControlFlowNode for request | ArbitraryFileReadAndDelete.py:41:16:41:27 | ControlFlowNode for Attribute | +| ArbitraryFileReadAndDelete.py:41:16:41:27 | ControlFlowNode for Attribute | ArbitraryFileReadAndDelete.py:42:41:42:64 | ControlFlowNode for BinaryExpr | +| ArbitraryFileReadAndDelete.py:47:16:47:22 | ControlFlowNode for request | ArbitraryFileReadAndDelete.py:47:16:47:27 | ControlFlowNode for Attribute | +| ArbitraryFileReadAndDelete.py:47:16:47:27 | ControlFlowNode for Attribute | ArbitraryFileReadAndDelete.py:48:33:48:40 | ControlFlowNode for filename | +| ArbitraryFileReadAndDelete.py:53:16:53:22 | ControlFlowNode for request | ArbitraryFileReadAndDelete.py:53:16:53:27 | ControlFlowNode for Attribute | +| ArbitraryFileReadAndDelete.py:53:16:53:27 | ControlFlowNode for Attribute | ArbitraryFileReadAndDelete.py:54:26:54:33 | ControlFlowNode for filename | +| ArbitraryFileReadAndDelete.py:59:16:59:22 | ControlFlowNode for request | ArbitraryFileReadAndDelete.py:59:16:59:27 | ControlFlowNode for Attribute | +| ArbitraryFileReadAndDelete.py:59:16:59:27 | ControlFlowNode for Attribute | ArbitraryFileReadAndDelete.py:60:18:60:25 | ControlFlowNode for filename | +| ArbitraryFileReadAndDelete.py:74:16:74:22 | ControlFlowNode for request | ArbitraryFileReadAndDelete.py:74:16:74:27 | ControlFlowNode for Attribute | +| ArbitraryFileReadAndDelete.py:74:16:74:27 | ControlFlowNode for Attribute | ArbitraryFileReadAndDelete.py:77:28:77:35 | ControlFlowNode for filename | +| ArbitraryFileReadAndDelete.py:85:16:85:22 | ControlFlowNode for request | ArbitraryFileReadAndDelete.py:85:16:85:27 | ControlFlowNode for Attribute | +| ArbitraryFileReadAndDelete.py:85:16:85:27 | ControlFlowNode for Attribute | ArbitraryFileReadAndDelete.py:88:32:88:39 | ControlFlowNode for filename | +| ArbitraryFileReadAndDelete.py:95:16:95:22 | ControlFlowNode for request | ArbitraryFileReadAndDelete.py:95:16:95:27 | ControlFlowNode for Attribute | +| ArbitraryFileReadAndDelete.py:95:16:95:27 | ControlFlowNode for Attribute | ArbitraryFileReadAndDelete.py:98:28:98:35 | ControlFlowNode for filename | +nodes +| ArbitraryFileReadAndDelete.py:20:16:20:22 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | +| ArbitraryFileReadAndDelete.py:20:16:20:27 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute | +| ArbitraryFileReadAndDelete.py:21:23:21:30 | ControlFlowNode for filename | semmle.label | ControlFlowNode for filename | +| ArbitraryFileReadAndDelete.py:27:16:27:22 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | +| ArbitraryFileReadAndDelete.py:27:16:27:27 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute | +| ArbitraryFileReadAndDelete.py:28:20:28:27 | ControlFlowNode for filename | semmle.label | ControlFlowNode for filename | +| ArbitraryFileReadAndDelete.py:33:16:33:22 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | +| ArbitraryFileReadAndDelete.py:33:16:33:27 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute | +| ArbitraryFileReadAndDelete.py:35:24:35:31 | ControlFlowNode for filename | semmle.label | ControlFlowNode for filename | +| ArbitraryFileReadAndDelete.py:41:16:41:22 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | +| ArbitraryFileReadAndDelete.py:41:16:41:27 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute | +| ArbitraryFileReadAndDelete.py:42:41:42:64 | ControlFlowNode for BinaryExpr | semmle.label | ControlFlowNode for BinaryExpr | +| ArbitraryFileReadAndDelete.py:47:16:47:22 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | +| ArbitraryFileReadAndDelete.py:47:16:47:27 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute | +| ArbitraryFileReadAndDelete.py:48:33:48:40 | ControlFlowNode for filename | semmle.label | ControlFlowNode for filename | +| ArbitraryFileReadAndDelete.py:53:16:53:22 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | +| ArbitraryFileReadAndDelete.py:53:16:53:27 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute | +| ArbitraryFileReadAndDelete.py:54:26:54:33 | ControlFlowNode for filename | semmle.label | ControlFlowNode for filename | +| ArbitraryFileReadAndDelete.py:59:16:59:22 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | +| ArbitraryFileReadAndDelete.py:59:16:59:27 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute | +| ArbitraryFileReadAndDelete.py:60:18:60:25 | ControlFlowNode for filename | semmle.label | ControlFlowNode for filename | +| ArbitraryFileReadAndDelete.py:74:16:74:22 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | +| ArbitraryFileReadAndDelete.py:74:16:74:27 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute | +| ArbitraryFileReadAndDelete.py:77:28:77:35 | ControlFlowNode for filename | semmle.label | ControlFlowNode for filename | +| ArbitraryFileReadAndDelete.py:85:16:85:22 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | +| ArbitraryFileReadAndDelete.py:85:16:85:27 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute | +| ArbitraryFileReadAndDelete.py:88:32:88:39 | ControlFlowNode for filename | semmle.label | ControlFlowNode for filename | +| ArbitraryFileReadAndDelete.py:95:16:95:22 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | +| ArbitraryFileReadAndDelete.py:95:16:95:27 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute | +| ArbitraryFileReadAndDelete.py:98:28:98:35 | ControlFlowNode for filename | semmle.label | ControlFlowNode for filename | +subpaths +#select +| ArbitraryFileReadAndDelete.py:21:23:21:30 | ControlFlowNode for filename | ArbitraryFileReadAndDelete.py:20:16:20:22 | ControlFlowNode for request | ArbitraryFileReadAndDelete.py:21:23:21:30 | ControlFlowNode for filename | Arbitrary file read and delete might include code from $@. | ArbitraryFileReadAndDelete.py:20:16:20:22 | ControlFlowNode for request | this user input | +| ArbitraryFileReadAndDelete.py:28:20:28:27 | ControlFlowNode for filename | ArbitraryFileReadAndDelete.py:27:16:27:22 | ControlFlowNode for request | ArbitraryFileReadAndDelete.py:28:20:28:27 | ControlFlowNode for filename | Arbitrary file read and delete might include code from $@. | ArbitraryFileReadAndDelete.py:27:16:27:22 | ControlFlowNode for request | this user input | +| ArbitraryFileReadAndDelete.py:35:24:35:31 | ControlFlowNode for filename | ArbitraryFileReadAndDelete.py:33:16:33:22 | ControlFlowNode for request | ArbitraryFileReadAndDelete.py:35:24:35:31 | ControlFlowNode for filename | Arbitrary file read and delete might include code from $@. | ArbitraryFileReadAndDelete.py:33:16:33:22 | ControlFlowNode for request | this user input | +| ArbitraryFileReadAndDelete.py:42:41:42:64 | ControlFlowNode for BinaryExpr | ArbitraryFileReadAndDelete.py:41:16:41:22 | ControlFlowNode for request | ArbitraryFileReadAndDelete.py:42:41:42:64 | ControlFlowNode for BinaryExpr | Arbitrary file read and delete might include code from $@. | ArbitraryFileReadAndDelete.py:41:16:41:22 | ControlFlowNode for request | this user input | +| ArbitraryFileReadAndDelete.py:48:33:48:40 | ControlFlowNode for filename | ArbitraryFileReadAndDelete.py:47:16:47:22 | ControlFlowNode for request | ArbitraryFileReadAndDelete.py:48:33:48:40 | ControlFlowNode for filename | Arbitrary file read and delete might include code from $@. | ArbitraryFileReadAndDelete.py:47:16:47:22 | ControlFlowNode for request | this user input | +| ArbitraryFileReadAndDelete.py:54:26:54:33 | ControlFlowNode for filename | ArbitraryFileReadAndDelete.py:53:16:53:22 | ControlFlowNode for request | ArbitraryFileReadAndDelete.py:54:26:54:33 | ControlFlowNode for filename | Arbitrary file read and delete might include code from $@. | ArbitraryFileReadAndDelete.py:53:16:53:22 | ControlFlowNode for request | this user input | +| ArbitraryFileReadAndDelete.py:60:18:60:25 | ControlFlowNode for filename | ArbitraryFileReadAndDelete.py:59:16:59:22 | ControlFlowNode for request | ArbitraryFileReadAndDelete.py:60:18:60:25 | ControlFlowNode for filename | Arbitrary file read and delete might include code from $@. | ArbitraryFileReadAndDelete.py:59:16:59:22 | ControlFlowNode for request | this user input | +| ArbitraryFileReadAndDelete.py:77:28:77:35 | ControlFlowNode for filename | ArbitraryFileReadAndDelete.py:74:16:74:22 | ControlFlowNode for request | ArbitraryFileReadAndDelete.py:77:28:77:35 | ControlFlowNode for filename | Arbitrary file read and delete might include code from $@. | ArbitraryFileReadAndDelete.py:74:16:74:22 | ControlFlowNode for request | this user input | +| ArbitraryFileReadAndDelete.py:88:32:88:39 | ControlFlowNode for filename | ArbitraryFileReadAndDelete.py:85:16:85:22 | ControlFlowNode for request | ArbitraryFileReadAndDelete.py:88:32:88:39 | ControlFlowNode for filename | Arbitrary file read and delete might include code from $@. | ArbitraryFileReadAndDelete.py:85:16:85:22 | ControlFlowNode for request | this user input | +| ArbitraryFileReadAndDelete.py:98:28:98:35 | ControlFlowNode for filename | ArbitraryFileReadAndDelete.py:95:16:95:22 | ControlFlowNode for request | ArbitraryFileReadAndDelete.py:98:28:98:35 | ControlFlowNode for filename | Arbitrary file read and delete might include code from $@. | ArbitraryFileReadAndDelete.py:95:16:95:22 | ControlFlowNode for request | this user input | diff --git a/python/ql/test/experimental/query-tests/Security/CWE-073/ArbitraryFileReadAndDelete.py b/python/ql/test/experimental/query-tests/Security/CWE-073/ArbitraryFileReadAndDelete.py new file mode 100644 index 000000000000..7da8d00c5bee --- /dev/null +++ b/python/ql/test/experimental/query-tests/Security/CWE-073/ArbitraryFileReadAndDelete.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +""" +@Desc : +""" +from fastapi import FastAPI, Request +from flask import Flask, request, send_file, make_response, helpers, jsonify, render_template,render_template_string, Response +from flask.json import jsonify +from pathlib import Path +from werkzeug.utils import secure_filename +import io +import os +import builtins + +app = Flask(__name__) + + +@app.route('/fileRead1') +def fileRead1(): + filename = request.args.get('filename') + context = io.open(filename) + response = make_response(context) + return response + +@app.route('/fileRead2') +def fileRead2(): + filename = request.args.get('filename') + context = open(filename, encoding="utf-8").read() + return context + +@app.route('/fileRead3') +def fileRead3(): + filename = request.args.get('filename') + context = "" + with builtins.open(filename, encoding="utf-8") as f: + context = f.readlines() + return context + +@app.route('/fileRead4') +def fileRead4(): + filename = request.args.get('filename') + response = send_file(path_or_file = "/home/work/" + filename) + return response + +@app.route('/fileRead5') +def fileRead5(): + filename = request.args.get('filename') + context = helpers.send_file(filename) + return context + +@app.route('/fileRead6') +def fileRead6(): + filename = request.args.get('filename') + context = os.listdir(filename) + return render_template_string("filedelete.html", contents = context) + +@app.route('/fileRead7') +def fileRead7(): + filename = request.args.get('filename') + fd = os.open(filename, os.O_RDWR|os.O_CREAT) + result = os.fdopen(fd).read() + return jsonify(result) + +@app.route('/fileRead8') +def fileRead8(): + filename = request.args.get('filename') + result = "" + for dir in os.scandir(filename): + result += str(dir) + return render_template_string(filename) + +@app.route('/fileDelete1') +def fileDelete1(): + filename = request.args.get('filename') + result = "" + try: + result = os.remove(filename) + except Exception as e: + result = "failed" + response = make_response(result) + return response + +@app.route('/fileDelete2') +def fileDelete2(): + filename = request.args.get('filename') + result = "" + try: + result = os.removedirs(filename) + except Exception as e: + result = "failed" + return result, 200 + +@app.route('/fileDelete3') +def fileDelete3(): + filename = request.args.get('filename') + result = "" + try: + result = os.unlink(filename) + except Exception as e: + result = "failed" + return render_template(template_name_or_list = "filedelete.html", contents = filename) + +@app.route('/good1') +def good1(): + filename = request.args.get('filename') + sec_filename = secure_filename(filename) + try: + result = os.unlink(sec_filename) + except Exception as e: + result = "failed" + +if __name__ == '__main__': + app.debug = True + app.run(host = '0.0.0.0') \ No newline at end of file diff --git a/python/ql/test/experimental/query-tests/Security/CWE-073/ArbitraryFileReadAndDelete.qlref b/python/ql/test/experimental/query-tests/Security/CWE-073/ArbitraryFileReadAndDelete.qlref new file mode 100644 index 000000000000..8e11f75407ea --- /dev/null +++ b/python/ql/test/experimental/query-tests/Security/CWE-073/ArbitraryFileReadAndDelete.qlref @@ -0,0 +1 @@ +experimental/Security/CWE-073/ArbitraryFileReadAndDelete.ql \ No newline at end of file