diff --git a/python/ql/lib/change-notes/2022-09-22-flask-jsonify.md b/python/ql/lib/change-notes/2022-09-22-flask-jsonify.md new file mode 100644 index 00000000000..cac16e270f4 --- /dev/null +++ b/python/ql/lib/change-notes/2022-09-22-flask-jsonify.md @@ -0,0 +1,4 @@ +--- +category: minorAnalysis +--- +* Added modeling of creating Flask responses with `flask.jsonify`. diff --git a/python/ql/lib/semmle/python/frameworks/Flask.qll b/python/ql/lib/semmle/python/frameworks/Flask.qll index 890e45a2ab5..2f272723182 100644 --- a/python/ql/lib/semmle/python/frameworks/Flask.qll +++ b/python/ql/lib/semmle/python/frameworks/Flask.qll @@ -171,6 +171,22 @@ module Flask { override DataFlow::Node getMimetypeOrContentTypeArg() { none() } } + /** + * A call to `flask.jsonify` function. This creates a JSON response. + * + * See + * - https://flask.palletsprojects.com/en/2.2.x/api/#flask.json.jsonify + */ + private class FlaskJsonifyCall extends InstanceSource, DataFlow::CallCfgNode { + FlaskJsonifyCall() { this = API::moduleImport("flask").getMember("jsonify").getACall() } + + override DataFlow::Node getBody() { result in [this.getArg(_), this.getArgByName(_)] } + + override string getMimetypeDefault() { result = "application/json" } + + override DataFlow::Node getMimetypeOrContentTypeArg() { none() } + } + /** Gets a reference to an instance of `flask.Response`. */ private DataFlow::TypeTrackingNode instance(DataFlow::TypeTracker t) { t.start() and diff --git a/python/ql/test/library-tests/frameworks/flask/response_test.py b/python/ql/test/library-tests/frameworks/flask/response_test.py index 81b73fd4367..39373eac9fe 100644 --- a/python/ql/test/library-tests/frameworks/flask/response_test.py +++ b/python/ql/test/library-tests/frameworks/flask/response_test.py @@ -66,8 +66,8 @@ def html8(): # $requestHandler @app.route("/jsonify") # $routeSetup="/jsonify" def jsonify_route(): # $requestHandler - data = {"foo": "bar"} - resp = jsonify(data) # $ MISSING: HttpResponse mimetype=application/json responseBody=data + x = "x"; y = "y"; z = "z" + resp = jsonify(x, y, z=z) # $ HttpResponse mimetype=application/json responseBody=x responseBody=y responseBody=z return resp # $ SPURIOUS: HttpResponse mimetype=text/html responseBody=resp ################################################################################ diff --git a/python/ql/test/query-tests/Security/CWE-209-StackTraceExposure/StackTraceExposure.expected b/python/ql/test/query-tests/Security/CWE-209-StackTraceExposure/StackTraceExposure.expected index 12bda8e4bea..07b208caaac 100644 --- a/python/ql/test/query-tests/Security/CWE-209-StackTraceExposure/StackTraceExposure.expected +++ b/python/ql/test/query-tests/Security/CWE-209-StackTraceExposure/StackTraceExposure.expected @@ -5,6 +5,7 @@ edges | test.py:50:29:50:31 | ControlFlowNode for err | test.py:50:16:50:32 | ControlFlowNode for format_error() | | test.py:50:29:50:31 | ControlFlowNode for err | test.py:52:18:52:20 | ControlFlowNode for msg | | test.py:52:18:52:20 | ControlFlowNode for msg | test.py:53:12:53:27 | ControlFlowNode for BinaryExpr | +| test.py:65:25:65:25 | SSA variable e | test.py:66:24:66:40 | ControlFlowNode for Dict | nodes | test.py:16:16:16:37 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() | | test.py:23:25:23:25 | SSA variable e | semmle.label | SSA variable e | @@ -16,6 +17,8 @@ nodes | test.py:50:29:50:31 | ControlFlowNode for err | semmle.label | ControlFlowNode for err | | test.py:52:18:52:20 | ControlFlowNode for msg | semmle.label | ControlFlowNode for msg | | test.py:53:12:53:27 | ControlFlowNode for BinaryExpr | semmle.label | ControlFlowNode for BinaryExpr | +| test.py:65:25:65:25 | SSA variable e | semmle.label | SSA variable e | +| test.py:66:24:66:40 | ControlFlowNode for Dict | semmle.label | ControlFlowNode for Dict | subpaths | test.py:50:29:50:31 | ControlFlowNode for err | test.py:52:18:52:20 | ControlFlowNode for msg | test.py:53:12:53:27 | ControlFlowNode for BinaryExpr | test.py:50:16:50:32 | ControlFlowNode for format_error() | #select @@ -23,3 +26,4 @@ subpaths | test.py:24:16:24:16 | ControlFlowNode for e | test.py:23:25:23:25 | SSA variable e | test.py:24:16:24:16 | ControlFlowNode for e | $@ flows to this location and may be exposed to an external user. | test.py:23:25:23:25 | SSA variable e | Stack trace information | | test.py:32:16:32:30 | ControlFlowNode for Attribute | test.py:31:25:31:25 | SSA variable e | test.py:32:16:32:30 | ControlFlowNode for Attribute | $@ flows to this location and may be exposed to an external user. | test.py:31:25:31:25 | SSA variable e | Stack trace information | | test.py:50:16:50:32 | ControlFlowNode for format_error() | test.py:49:15:49:36 | ControlFlowNode for Attribute() | test.py:50:16:50:32 | ControlFlowNode for format_error() | $@ flows to this location and may be exposed to an external user. | test.py:49:15:49:36 | ControlFlowNode for Attribute() | Stack trace information | +| test.py:66:24:66:40 | ControlFlowNode for Dict | test.py:65:25:65:25 | SSA variable e | test.py:66:24:66:40 | ControlFlowNode for Dict | $@ flows to this location and may be exposed to an external user. | test.py:65:25:65:25 | SSA variable e | Stack trace information | diff --git a/python/ql/test/query-tests/Security/CWE-209-StackTraceExposure/test.py b/python/ql/test/query-tests/Security/CWE-209-StackTraceExposure/test.py index f67f532ce25..3bcafbaf5dc 100644 --- a/python/ql/test/query-tests/Security/CWE-209-StackTraceExposure/test.py +++ b/python/ql/test/query-tests/Security/CWE-209-StackTraceExposure/test.py @@ -1,4 +1,4 @@ -from flask import Flask, request, make_response +from flask import Flask, request, make_response, jsonify app = Flask(__name__) @@ -56,3 +56,15 @@ def format_error(msg): @app.route('/maybe_xss') def maybe_xss(): return make_response(request.args.get('name', '')) + +# BAD +@app.route('/bad/jsonify') +def bad_jsonify(): + try: + do_computation() + except Exception as e: # $ exceptionInfo + return jsonify({"error": str(e)}) + + +if __name__ == "__main__": + app.run(debug=True)