diff --git a/python/ql/src/experimental/Security/CWE-113/HeaderInjection.qhelp b/python/ql/src/experimental/Security/CWE-113/HeaderInjection.qhelp new file mode 100644 index 00000000000..33337294b9a --- /dev/null +++ b/python/ql/src/experimental/Security/CWE-113/HeaderInjection.qhelp @@ -0,0 +1,26 @@ + + + +

If an HTTP Header is built using string concatenation or string formatting, and the +components of the concatenation include user input, a user +is likely to be able to manipulate the response.

+
+ + +

User input should not be included in an HTTP Header.

+
+ + +

In the following example, the code appends a user-provided value into a header.

+ + +
+ + +
  • OWASP: HTTP Response Splitting.
  • +
  • Python Security: HTTP header injection.
  • +
  • SonarSource: RSPEC-5167.
  • +
    +
    diff --git a/python/ql/src/experimental/Security/CWE-113/HeaderInjection.ql b/python/ql/src/experimental/Security/CWE-113/HeaderInjection.ql new file mode 100644 index 00000000000..3cb4a20d5de --- /dev/null +++ b/python/ql/src/experimental/Security/CWE-113/HeaderInjection.ql @@ -0,0 +1,21 @@ +/** + * @name HTTP Header Injection + * @description User input should not be used in HTTP headers, otherwise a malicious user + * may be able to inject a value that could manipulate the response. + * @kind path-problem + * @problem.severity error + * @id py/header-injection + * @tags security + * external/cwe/cwe-113 + * external/cwe/cwe-079 + */ + +// determine precision above +import python +import experimental.semmle.python.security.injection.HTTPHeaders +import DataFlow::PathGraph + +from HeaderInjectionFlowConfig config, DataFlow::PathNode source, DataFlow::PathNode sink +where config.hasFlowPath(source, sink) +select sink.getNode(), source, sink, "$@ HTTP header is constructed from a $@.", sink.getNode(), + "This", source.getNode(), "user-provided value" diff --git a/python/ql/src/experimental/Security/CWE-113/header_injection.py b/python/ql/src/experimental/Security/CWE-113/header_injection.py new file mode 100644 index 00000000000..117383710e3 --- /dev/null +++ b/python/ql/src/experimental/Security/CWE-113/header_injection.py @@ -0,0 +1,9 @@ +from flask import Response, request, Flask, make_response + + +@app.route("/flask_Response") +def flask_Response(): + rfs_header = request.args["rfs_header"] + response = Response() + response.headers['HeaderName'] = rfs_header + return response diff --git a/python/ql/src/experimental/semmle/python/Concepts.qll b/python/ql/src/experimental/semmle/python/Concepts.qll index 21c202851f1..2e57e0dba97 100644 --- a/python/ql/src/experimental/semmle/python/Concepts.qll +++ b/python/ql/src/experimental/semmle/python/Concepts.qll @@ -209,3 +209,36 @@ class SQLEscape extends DataFlow::Node { */ DataFlow::Node getAnInput() { result = range.getAnInput() } } + +/** Provides classes for modeling HTTP Header APIs. */ +module HeaderDeclaration { + /** + * A data-flow node that collects functions setting HTTP Headers' content. + * + * Extend this class to model new APIs. If you want to refine existing API models, + * extend `HeaderDeclaration` instead. + */ + abstract class Range extends DataFlow::Node { + /** + * Gets the argument containing the header value. + */ + abstract DataFlow::Node getAnInput(); + } +} + +/** + * A data-flow node that collects functions setting HTTP Headers' content. + * + * Extend this class to model new APIs. If you want to refine existing API models, + * extend `HeaderDeclaration` instead. + */ +class HeaderDeclaration extends DataFlow::Node { + HeaderDeclaration::Range range; + + HeaderDeclaration() { this = range } + + /** + * Gets the argument containing the header value. + */ + DataFlow::Node getAnInput() { result = range.getAnInput() } +} diff --git a/python/ql/src/experimental/semmle/python/Frameworks.qll b/python/ql/src/experimental/semmle/python/Frameworks.qll index 5a77fc63a7d..bb548e713fe 100644 --- a/python/ql/src/experimental/semmle/python/Frameworks.qll +++ b/python/ql/src/experimental/semmle/python/Frameworks.qll @@ -3,4 +3,7 @@ */ private import experimental.semmle.python.frameworks.Stdlib +private import experimental.semmle.python.frameworks.Flask +private import experimental.semmle.python.frameworks.Django +private import experimental.semmle.python.frameworks.Werkzeug private import experimental.semmle.python.frameworks.LDAP diff --git a/python/ql/src/experimental/semmle/python/frameworks/Django.qll b/python/ql/src/experimental/semmle/python/frameworks/Django.qll new file mode 100644 index 00000000000..68153dfae00 --- /dev/null +++ b/python/ql/src/experimental/semmle/python/frameworks/Django.qll @@ -0,0 +1,78 @@ +/** + * Provides classes modeling security-relevant aspects of the `django` PyPI package. + * See https://www.djangoproject.com/. + */ + +private import python +private import semmle.python.frameworks.Django +private import semmle.python.dataflow.new.DataFlow +private import experimental.semmle.python.Concepts +private import semmle.python.ApiGraphs + +private module PrivateDjango { + API::Node django() { result = API::moduleImport("django") } + + private module django { + API::Node http() { result = django().getMember("http") } + + module http { + API::Node response() { result = http().getMember("response") } + + module response { + module HttpResponse { + API::Node baseClassRef() { + result = response().getMember("HttpResponse").getReturn() + or + // Handle `django.http.HttpResponse` alias + result = http().getMember("HttpResponse").getReturn() + } + + /** Gets a reference to a header instance. */ + private DataFlow::LocalSourceNode headerInstance(DataFlow::TypeTracker t) { + t.start() and + ( + exists(SubscriptNode subscript | + subscript.getObject() = baseClassRef().getAUse().asCfgNode() and + result.asCfgNode() = subscript + ) + or + result.(DataFlow::AttrRead).getObject() = baseClassRef().getAUse() + ) + or + exists(DataFlow::TypeTracker t2 | result = headerInstance(t2).track(t2, t)) + } + + /** Gets a reference to a header instance use. */ + private DataFlow::Node headerInstance() { + headerInstance(DataFlow::TypeTracker::end()).flowsTo(result) + } + + /** Gets a reference to a header instance call with `__setitem__`. */ + private DataFlow::Node headerSetItemCall() { + result = headerInstance() and + result.(DataFlow::AttrRead).getAttributeName() = "__setitem__" + } + + class DjangoResponseSetItemCall extends DataFlow::CallCfgNode, HeaderDeclaration::Range { + DjangoResponseSetItemCall() { this.getFunction() = headerSetItemCall() } + + override DataFlow::Node getAnInput() { result = this.getArg([0, 1]) } + } + + class DjangoResponseDefinition extends DataFlow::Node, HeaderDeclaration::Range { + DataFlow::Node headerInput; + + DjangoResponseDefinition() { + this.asCfgNode().(DefinitionNode) = headerInstance().asCfgNode() and + headerInput.asCfgNode() = this.asCfgNode().(DefinitionNode).getValue() + } + + override DataFlow::Node getAnInput() { + result.asExpr() in [headerInput.asExpr(), this.asExpr().(Subscript).getIndex()] + } + } + } + } + } + } +} diff --git a/python/ql/src/experimental/semmle/python/frameworks/Flask.qll b/python/ql/src/experimental/semmle/python/frameworks/Flask.qll new file mode 100644 index 00000000000..a62c38bb060 --- /dev/null +++ b/python/ql/src/experimental/semmle/python/frameworks/Flask.qll @@ -0,0 +1,73 @@ +/** + * Provides classes modeling security-relevant aspects of the `flask` PyPI package. + * See https://flask.palletsprojects.com/en/1.1.x/. + */ + +private import python +private import semmle.python.frameworks.Flask +private import semmle.python.dataflow.new.DataFlow +private import experimental.semmle.python.Concepts +private import semmle.python.ApiGraphs + +module ExperimentalFlask { + /** + * A reference to either `flask.make_response` function, or the `make_response` method on + * an instance of `flask.Flask`. This creates an instance of the `flask_response` + * class (class-attribute on a flask application), which by default is + * `flask.Response`. + * + * See + * - https://flask.palletsprojects.com/en/1.1.x/api/#flask.Flask.make_response + * - https://flask.palletsprojects.com/en/1.1.x/api/#flask.make_response + */ + private API::Node flaskMakeResponse() { + result = + [API::moduleImport("flask"), Flask::FlaskApp::instance()] + .getMember(["make_response", "jsonify", "make_default_options_response"]) + } + + /** Gets a reference to a header instance. */ + private DataFlow::LocalSourceNode headerInstance(DataFlow::TypeTracker t) { + t.start() and + result.(DataFlow::AttrRead).getObject().getALocalSource() = + [Flask::Response::classRef(), flaskMakeResponse()].getReturn().getAUse() + or + exists(DataFlow::TypeTracker t2 | result = headerInstance(t2).track(t2, t)) + } + + /** Gets a reference to a header instance use. */ + private DataFlow::Node headerInstance() { + headerInstance(DataFlow::TypeTracker::end()).flowsTo(result) + } + + /** Gets a reference to a header instance call/subscript */ + private DataFlow::Node headerInstanceCall() { + headerInstance() in [result.(DataFlow::AttrRead), result.(DataFlow::AttrRead).getObject()] or + headerInstance().asExpr() = result.asExpr().(Subscript).getObject() + } + + class FlaskHeaderDefinition extends DataFlow::Node, HeaderDeclaration::Range { + DataFlow::Node headerInput; + + FlaskHeaderDefinition() { + this.asCfgNode().(DefinitionNode) = headerInstanceCall().asCfgNode() and + headerInput.asCfgNode() = this.asCfgNode().(DefinitionNode).getValue() + } + + override DataFlow::Node getAnInput() { + result.asExpr() in [headerInput.asExpr(), this.asExpr().(Subscript).getIndex()] + } + } + + private class FlaskMakeResponseExtend extends DataFlow::CallCfgNode, HeaderDeclaration::Range { + FlaskMakeResponseExtend() { this.getFunction() = headerInstanceCall() } + + override DataFlow::Node getAnInput() { result = this.getArg(_) } + } + + private class FlaskResponse extends DataFlow::CallCfgNode, HeaderDeclaration::Range { + FlaskResponse() { this = Flask::Response::classRef().getACall() } + + override DataFlow::Node getAnInput() { result = this.getArgByName("headers") } + } +} diff --git a/python/ql/src/experimental/semmle/python/frameworks/Werkzeug.qll b/python/ql/src/experimental/semmle/python/frameworks/Werkzeug.qll new file mode 100644 index 00000000000..d37fefe2af8 --- /dev/null +++ b/python/ql/src/experimental/semmle/python/frameworks/Werkzeug.qll @@ -0,0 +1,31 @@ +/** + * Provides classes modeling security-relevant aspects of the `Werkzeug` PyPI package. + * See + * - https://pypi.org/project/Werkzeug/ + * - https://werkzeug.palletsprojects.com/en/1.0.x/#werkzeug + */ + +private import python +private import semmle.python.frameworks.Flask +private import semmle.python.dataflow.new.DataFlow +private import experimental.semmle.python.Concepts +private import semmle.python.ApiGraphs + +private module Werkzeug { + module datastructures { + module Headers { + class WerkzeugHeaderAddCall extends DataFlow::CallCfgNode, HeaderDeclaration::Range { + WerkzeugHeaderAddCall() { + this.getFunction().(DataFlow::AttrRead).getObject().getALocalSource() = + API::moduleImport("werkzeug") + .getMember("datastructures") + .getMember("Headers") + .getACall() and + this.getFunction().(DataFlow::AttrRead).getAttributeName() = "add" + } + + override DataFlow::Node getAnInput() { result = this.getArg(_) } + } + } + } +} diff --git a/python/ql/src/experimental/semmle/python/security/injection/HTTPHeaders.qll b/python/ql/src/experimental/semmle/python/security/injection/HTTPHeaders.qll new file mode 100644 index 00000000000..d31a7d5ac9d --- /dev/null +++ b/python/ql/src/experimental/semmle/python/security/injection/HTTPHeaders.qll @@ -0,0 +1,18 @@ +import python +import experimental.semmle.python.Concepts +import semmle.python.dataflow.new.DataFlow +import semmle.python.dataflow.new.TaintTracking +import semmle.python.dataflow.new.RemoteFlowSources + +/** + * A taint-tracking configuration for detecting HTTP Header injections. + */ +class HeaderInjectionFlowConfig extends TaintTracking::Configuration { + HeaderInjectionFlowConfig() { this = "HeaderInjectionFlowConfig" } + + override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource } + + override predicate isSink(DataFlow::Node sink) { + sink = any(HeaderDeclaration headerDeclaration).getAnInput() + } +} diff --git a/python/ql/test/experimental/query-tests/Security/CWE-113/HeaderInjection.expected b/python/ql/test/experimental/query-tests/Security/CWE-113/HeaderInjection.expected new file mode 100644 index 00000000000..9e0eff704b6 --- /dev/null +++ b/python/ql/test/experimental/query-tests/Security/CWE-113/HeaderInjection.expected @@ -0,0 +1,43 @@ +edges +| flask_bad.py:9:18:9:24 | ControlFlowNode for request | flask_bad.py:9:18:9:29 | ControlFlowNode for Attribute | +| flask_bad.py:9:18:9:29 | ControlFlowNode for Attribute | flask_bad.py:9:18:9:43 | ControlFlowNode for Subscript | +| flask_bad.py:9:18:9:43 | ControlFlowNode for Subscript | flask_bad.py:12:31:12:40 | ControlFlowNode for rfs_header | +| flask_bad.py:19:18:19:24 | ControlFlowNode for request | flask_bad.py:19:18:19:29 | ControlFlowNode for Attribute | +| flask_bad.py:19:18:19:29 | ControlFlowNode for Attribute | flask_bad.py:19:18:19:43 | ControlFlowNode for Subscript | +| flask_bad.py:19:18:19:43 | ControlFlowNode for Subscript | flask_bad.py:21:38:21:47 | ControlFlowNode for rfs_header | +| flask_bad.py:27:18:27:24 | ControlFlowNode for request | flask_bad.py:27:18:27:29 | ControlFlowNode for Attribute | +| flask_bad.py:27:18:27:29 | ControlFlowNode for Attribute | flask_bad.py:27:18:27:43 | ControlFlowNode for Subscript | +| flask_bad.py:27:18:27:43 | ControlFlowNode for Subscript | flask_bad.py:29:34:29:43 | ControlFlowNode for rfs_header | +| flask_bad.py:35:18:35:24 | ControlFlowNode for request | flask_bad.py:35:18:35:29 | ControlFlowNode for Attribute | +| flask_bad.py:35:18:35:29 | ControlFlowNode for Attribute | flask_bad.py:35:18:35:43 | ControlFlowNode for Subscript | +| flask_bad.py:35:18:35:43 | ControlFlowNode for Subscript | flask_bad.py:38:9:38:34 | ControlFlowNode for Dict | +| flask_bad.py:44:44:44:50 | ControlFlowNode for request | flask_bad.py:44:44:44:55 | ControlFlowNode for Attribute | +| flask_bad.py:44:44:44:55 | ControlFlowNode for Attribute | flask_bad.py:44:44:44:69 | ControlFlowNode for Subscript | +| flask_bad.py:44:44:44:69 | ControlFlowNode for Subscript | flask_bad.py:44:29:44:70 | ControlFlowNode for Dict | +nodes +| flask_bad.py:9:18:9:24 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | +| flask_bad.py:9:18:9:29 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute | +| flask_bad.py:9:18:9:43 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript | +| flask_bad.py:12:31:12:40 | ControlFlowNode for rfs_header | semmle.label | ControlFlowNode for rfs_header | +| flask_bad.py:19:18:19:24 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | +| flask_bad.py:19:18:19:29 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute | +| flask_bad.py:19:18:19:43 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript | +| flask_bad.py:21:38:21:47 | ControlFlowNode for rfs_header | semmle.label | ControlFlowNode for rfs_header | +| flask_bad.py:27:18:27:24 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | +| flask_bad.py:27:18:27:29 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute | +| flask_bad.py:27:18:27:43 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript | +| flask_bad.py:29:34:29:43 | ControlFlowNode for rfs_header | semmle.label | ControlFlowNode for rfs_header | +| flask_bad.py:35:18:35:24 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | +| flask_bad.py:35:18:35:29 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute | +| flask_bad.py:35:18:35:43 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript | +| flask_bad.py:38:9:38:34 | ControlFlowNode for Dict | semmle.label | ControlFlowNode for Dict | +| flask_bad.py:44:29:44:70 | ControlFlowNode for Dict | semmle.label | ControlFlowNode for Dict | +| flask_bad.py:44:44:44:50 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | +| flask_bad.py:44:44:44:55 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute | +| flask_bad.py:44:44:44:69 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript | +#select +| flask_bad.py:12:31:12:40 | ControlFlowNode for rfs_header | flask_bad.py:9:18:9:24 | ControlFlowNode for request | flask_bad.py:12:31:12:40 | ControlFlowNode for rfs_header | $@ HTTP header is constructed from a $@. | flask_bad.py:12:31:12:40 | ControlFlowNode for rfs_header | This | flask_bad.py:9:18:9:24 | ControlFlowNode for request | user-provided value | +| flask_bad.py:21:38:21:47 | ControlFlowNode for rfs_header | flask_bad.py:19:18:19:24 | ControlFlowNode for request | flask_bad.py:21:38:21:47 | ControlFlowNode for rfs_header | $@ HTTP header is constructed from a $@. | flask_bad.py:21:38:21:47 | ControlFlowNode for rfs_header | This | flask_bad.py:19:18:19:24 | ControlFlowNode for request | user-provided value | +| flask_bad.py:29:34:29:43 | ControlFlowNode for rfs_header | flask_bad.py:27:18:27:24 | ControlFlowNode for request | flask_bad.py:29:34:29:43 | ControlFlowNode for rfs_header | $@ HTTP header is constructed from a $@. | flask_bad.py:29:34:29:43 | ControlFlowNode for rfs_header | This | flask_bad.py:27:18:27:24 | ControlFlowNode for request | user-provided value | +| flask_bad.py:38:9:38:34 | ControlFlowNode for Dict | flask_bad.py:35:18:35:24 | ControlFlowNode for request | flask_bad.py:38:9:38:34 | ControlFlowNode for Dict | $@ HTTP header is constructed from a $@. | flask_bad.py:38:9:38:34 | ControlFlowNode for Dict | This | flask_bad.py:35:18:35:24 | ControlFlowNode for request | user-provided value | +| flask_bad.py:44:29:44:70 | ControlFlowNode for Dict | flask_bad.py:44:44:44:50 | ControlFlowNode for request | flask_bad.py:44:29:44:70 | ControlFlowNode for Dict | $@ HTTP header is constructed from a $@. | flask_bad.py:44:29:44:70 | ControlFlowNode for Dict | This | flask_bad.py:44:44:44:50 | ControlFlowNode for request | user-provided value | diff --git a/python/ql/test/experimental/query-tests/Security/CWE-113/HeaderInjection.qlref b/python/ql/test/experimental/query-tests/Security/CWE-113/HeaderInjection.qlref new file mode 100644 index 00000000000..915175a7b6a --- /dev/null +++ b/python/ql/test/experimental/query-tests/Security/CWE-113/HeaderInjection.qlref @@ -0,0 +1 @@ +experimental/Security/CWE-113/HeaderInjection.ql diff --git a/python/ql/test/experimental/query-tests/Security/CWE-113/django_bad.py b/python/ql/test/experimental/query-tests/Security/CWE-113/django_bad.py new file mode 100644 index 00000000000..b97d9137f46 --- /dev/null +++ b/python/ql/test/experimental/query-tests/Security/CWE-113/django_bad.py @@ -0,0 +1,15 @@ +import django.http + + +def django_setitem(request): + rfs_header = request.GET.get("rfs_header") + response = django.http.HttpResponse() + response.__setitem__('HeaderName', rfs_header) + return response + + +def django_response(request): + rfs_header = request.GET.get("rfs_header") + response = django.http.HttpResponse() + response['HeaderName'] = rfs_header + return response diff --git a/python/ql/test/experimental/query-tests/Security/CWE-113/flask_bad.py b/python/ql/test/experimental/query-tests/Security/CWE-113/flask_bad.py new file mode 100644 index 00000000000..6f2968efb55 --- /dev/null +++ b/python/ql/test/experimental/query-tests/Security/CWE-113/flask_bad.py @@ -0,0 +1,47 @@ +from flask import Response, request, Flask, make_response +from werkzeug.datastructures import Headers + +app = Flask(__name__) + + +@app.route('/werkzeug_headers') +def werkzeug_headers(): + rfs_header = request.args["rfs_header"] + response = Response() + headers = Headers() + headers.add("HeaderName", rfs_header) + response.headers = headers + return response + + +@app.route("/flask_Response") +def flask_Response(): + rfs_header = request.args["rfs_header"] + response = Response() + response.headers['HeaderName'] = rfs_header + return response + + +@app.route("/flask_make_response") +def flask_make_response(): + rfs_header = request.args["rfs_header"] + resp = make_response("hello") + resp.headers['HeaderName'] = rfs_header + return resp + + +@app.route("/flask_make_response_extend") +def flask_make_response_extend(): + rfs_header = request.args["rfs_header"] + resp = make_response("hello") + resp.headers.extend( + {'HeaderName': rfs_header}) + return resp + + +@app.route("/Response_arg") +def Response_arg(): + return Response(headers={'HeaderName': request.args["rfs_header"]}) + +# if __name__ == "__main__": +# app.run(debug=True)