Merge remote-tracking branch 'origin/jorgectf/python/headerInjection' into jorgectf/python/insecure-cookie

This commit is contained in:
jorgectf
2021-07-25 03:22:16 +02:00
13 changed files with 398 additions and 0 deletions

View File

@@ -0,0 +1,26 @@
<!DOCTYPE qhelp PUBLIC
"-//Semmle//qhelp//EN"
"qhelp.dtd">
<qhelp>
<overview>
<p>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.</p>
</overview>
<recommendation>
<p>User input should not be included in an HTTP Header.</p>
</recommendation>
<example>
<p>In the following example, the code appends a user-provided value into a header.</p>
<sample src="header_injection.py" />
</example>
<references>
<li>OWASP: <a href="https://owasp.org/www-community/attacks/HTTP_Response_Splitting">HTTP Response Splitting</a>.</li>
<li>Python Security: <a href="https://python-security.readthedocs.io/vuln/http-header-injection.html">HTTP header injection</a>.</li>
<li>SonarSource: <a href="https://rules.sonarsource.com/python/RSPEC-5167">RSPEC-5167</a>.</li>
</references>
</qhelp>

View File

@@ -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"

View File

@@ -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

View File

@@ -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() }
}

View File

@@ -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

View File

@@ -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()]
}
}
}
}
}
}
}

View File

@@ -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") }
}
}

View File

@@ -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(_) }
}
}
}
}

View File

@@ -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()
}
}

View File

@@ -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 |

View File

@@ -0,0 +1 @@
experimental/Security/CWE-113/HeaderInjection.ql

View File

@@ -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

View File

@@ -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)