Implemented sinks for bulk header updates, and added corresponding tests.

This commit is contained in:
Joe Farebrother
2024-04-03 15:47:01 +01:00
parent 3e9341ff8a
commit a88ad62c00
7 changed files with 158 additions and 50 deletions

View File

@@ -1084,6 +1084,55 @@ module Http {
}
}
/**
* A data-flow node that sets multiple headers in an HTTP response using a dict.
*
* Extend this class to model new APIs. If you want to refine existing API models,
* extend `ResponseHeaderBulkWrite::Range` instead.
*/
class ResponseHeaderBulkWrite extends DataFlow::Node instanceof ResponseHeaderBulkWrite::Range {
/**
* Gets the argument containing the headers dictionary.
*/
DataFlow::Node geBulkArg() { result = super.getBulkArg() }
/**
* Holds if newlines are accepted in the header name argument.
*/
predicate nameAllowsNewline() { super.nameAllowsNewline() }
/**
* Holds if newlines are accepted in the header value argument.
*/
predicate valueAllowsNewline() { super.valueAllowsNewline() }
}
/** Provides a class for modelling bulk header writes on HTTP responses. */
module ResponseHeaderBulkWrite {
/**
*sets multiple headers in an HTTP response using a dict.
*
* Extend this class to model new APIs. If you want to refine existing API models,
* extend `ResponseHeaderBulkWrite` instead.
*/
abstract class Range extends DataFlow::Node {
/**
* Gets the argument containing the headers dictionary.
*/
abstract DataFlow::Node getBulkArg();
/**
* Holds if newlines are accepted in the header name argument.
*/
abstract predicate nameAllowsNewline();
/**
* Holds if newlines are accepted in the header value argument.
*/
abstract predicate valueAllowsNewline();
}
}
/**
* A data-flow node that sets a cookie in an HTTP response.
*

View File

@@ -222,14 +222,41 @@ module Flask {
DataFlow::Node instance() { instance(DataFlow::TypeTracker::end()).flowsTo(result) }
/** An `Headers` instance that is part of a Flask response. */
private class FlaskResponseHeadersInstances extends Werkzeug::Headers::InstanceSource
{
private class FlaskResponseHeadersInstances extends Werkzeug::Headers::InstanceSource {
FlaskResponseHeadersInstances() {
this.(DataFlow::AttrRead).getObject() = instance() and
this.(DataFlow::AttrRead).getAttributeName() = "headers"
}
}
// TODO: headers arg to make_response
/** A class instantiation of `Response` that sets response headers. */
private class ResponseClassHeadersWrite extends Http::Server::ResponseHeaderBulkWrite::Range,
ClassInstantiation
{
override DataFlow::Node getBulkArg() {
result = [this.getArg(2), this.getArgByName("headers")]
}
override predicate nameAllowsNewline() { any() }
override predicate valueAllowsNewline() { none() }
}
/** A call to `make_response that sets response headers. */
private class MakeResponseHeadersWrite extends Http::Server::ResponseHeaderBulkWrite::Range,
FlaskMakeResponseCall
{
override DataFlow::Node getBulkArg() {
result = this.getArg(2)
or
strictcount(this.getArg(_)) = 2 and
result = this.getArg(1)
}
override predicate nameAllowsNewline() { any() }
override predicate valueAllowsNewline() { none() }
}
}
// ---------------------------------------------------------------------------

View File

@@ -237,9 +237,21 @@ module Werkzeug {
override predicate valueAllowsNewline() { none() }
}
/** A call to `Headers.extend`, assumed to be a response header. */
private class HeaderExtendCall extends Http::Server::ResponseHeaderBulkWrite::Range,
DataFlow::MethodCallNode
{
HeaderExtendCall() { this.calls(instance(), "extend") }
override DataFlow::Node getBulkArg() { result = this.getArg(0) }
override predicate nameAllowsNewline() { any() }
override predicate valueAllowsNewline() { none() }
}
}
// TODO: `extend` bulk header update
/**
* Provides models for the `werkzeug.datastructures.Authorization` class
*

View File

@@ -50,4 +50,25 @@ module HttpHeaderInjection {
)
}
}
/** A key-value pair in a literal for a bulk header update, considered as a single header update. */
// TODO: We could instead consider bulk writes as sinks with an implicit read step of DictionaryKey/DictionaryValue content as needed.
private class HeaderBulkWriteDictLiteral extends Http::Server::ResponseHeaderWrite::Range instanceof Http::Server::ResponseHeaderBulkWrite
{
KeyValuePair item;
HeaderBulkWriteDictLiteral() { item = super.geBulkArg().asExpr().(Dict).getAnItem() }
override DataFlow::Node getNameArg() { result.asExpr() = item.getKey() }
override DataFlow::Node getValueArg() { result.asExpr() = item.getValue() }
override predicate nameAllowsNewline() {
Http::Server::ResponseHeaderBulkWrite.super.nameAllowsNewline()
}
override predicate valueAllowsNewline() {
Http::Server::ResponseHeaderBulkWrite.super.valueAllowsNewline()
}
}
}

View File

@@ -11,50 +11,6 @@ private import semmle.python.ApiGraphs
private import semmle.python.frameworks.Flask
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() {
result =
[Flask::Response::classRef(), flaskMakeResponse()]
.getReturn()
.getAMember()
.getAValueReachableFromSource()
}
/** 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()
}
private class FlaskResponse extends DataFlow::CallCfgNode, HeaderDeclaration::Range {
KeyValuePair item;
FlaskResponse() {
this = Flask::Response::classRef().getACall() and
item = this.getArg(_).asExpr().(Dict).getAnItem()
}
override DataFlow::Node getNameArg() { result.asExpr() = item.getKey() }
override DataFlow::Node getValueArg() { result.asExpr() = item.getValue() }
}
/**
* Gets a call to `set_cookie()`.
*

View File

@@ -3,12 +3,27 @@ edges
| flask_tests.py:1:29:1:35 | ControlFlowNode for request | flask_tests.py:9:18:9:24 | ControlFlowNode for request | provenance | |
| flask_tests.py:1:29:1:35 | ControlFlowNode for request | flask_tests.py:20:18:20:24 | ControlFlowNode for request | provenance | |
| flask_tests.py:1:29:1:35 | ControlFlowNode for request | flask_tests.py:29:18:29:24 | ControlFlowNode for request | provenance | |
| flask_tests.py:1:29:1:35 | ControlFlowNode for request | flask_tests.py:38:18:38:24 | ControlFlowNode for request | provenance | |
| flask_tests.py:1:29:1:35 | ControlFlowNode for request | flask_tests.py:49:44:49:50 | ControlFlowNode for request | provenance | |
| flask_tests.py:1:29:1:35 | ControlFlowNode for request | flask_tests.py:49:72:49:78 | ControlFlowNode for request | provenance | |
| flask_tests.py:1:29:1:35 | ControlFlowNode for request | flask_tests.py:53:18:53:24 | ControlFlowNode for request | provenance | |
| flask_tests.py:1:29:1:35 | ControlFlowNode for request | flask_tests.py:54:41:54:47 | ControlFlowNode for request | provenance | |
| flask_tests.py:1:29:1:35 | ControlFlowNode for request | flask_tests.py:59:18:59:24 | ControlFlowNode for request | provenance | |
| flask_tests.py:1:29:1:35 | ControlFlowNode for request | flask_tests.py:60:36:60:42 | ControlFlowNode for request | provenance | |
| flask_tests.py:9:5:9:14 | ControlFlowNode for rfs_header | flask_tests.py:13:17:13:26 | ControlFlowNode for rfs_header | provenance | |
| flask_tests.py:9:18:9:24 | ControlFlowNode for request | flask_tests.py:9:5:9:14 | ControlFlowNode for rfs_header | provenance | |
| flask_tests.py:20:5:20:14 | ControlFlowNode for rfs_header | flask_tests.py:23:22:23:31 | ControlFlowNode for rfs_header | provenance | |
| flask_tests.py:20:18:20:24 | ControlFlowNode for request | flask_tests.py:20:5:20:14 | ControlFlowNode for rfs_header | provenance | |
| flask_tests.py:29:5:29:14 | ControlFlowNode for rfs_header | flask_tests.py:32:22:32:31 | ControlFlowNode for rfs_header | provenance | |
| flask_tests.py:29:18:29:24 | ControlFlowNode for request | flask_tests.py:29:5:29:14 | ControlFlowNode for rfs_header | provenance | |
| flask_tests.py:38:5:38:14 | ControlFlowNode for rfs_header | flask_tests.py:43:10:43:19 | ControlFlowNode for rfs_header | provenance | |
| flask_tests.py:38:18:38:24 | ControlFlowNode for request | flask_tests.py:38:5:38:14 | ControlFlowNode for rfs_header | provenance | |
| flask_tests.py:49:44:49:50 | ControlFlowNode for request | flask_tests.py:49:72:49:97 | ControlFlowNode for Subscript | provenance | |
| flask_tests.py:49:72:49:78 | ControlFlowNode for request | flask_tests.py:49:72:49:97 | ControlFlowNode for Subscript | provenance | |
| flask_tests.py:53:18:53:24 | ControlFlowNode for request | flask_tests.py:54:41:54:66 | ControlFlowNode for Subscript | provenance | |
| flask_tests.py:54:41:54:47 | ControlFlowNode for request | flask_tests.py:54:41:54:66 | ControlFlowNode for Subscript | provenance | |
| flask_tests.py:59:18:59:24 | ControlFlowNode for request | flask_tests.py:60:36:60:61 | ControlFlowNode for Subscript | provenance | |
| flask_tests.py:60:36:60:42 | ControlFlowNode for request | flask_tests.py:60:36:60:61 | ControlFlowNode for Subscript | provenance | |
nodes
| flask_tests.py:1:29:1:35 | ControlFlowNode for ImportMember | semmle.label | ControlFlowNode for ImportMember |
| flask_tests.py:1:29:1:35 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
@@ -21,8 +36,24 @@ nodes
| flask_tests.py:29:5:29:14 | ControlFlowNode for rfs_header | semmle.label | ControlFlowNode for rfs_header |
| flask_tests.py:29:18:29:24 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
| flask_tests.py:32:22:32:31 | ControlFlowNode for rfs_header | semmle.label | ControlFlowNode for rfs_header |
| flask_tests.py:38:5:38:14 | ControlFlowNode for rfs_header | semmle.label | ControlFlowNode for rfs_header |
| flask_tests.py:38:18:38:24 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
| flask_tests.py:43:10:43:19 | ControlFlowNode for rfs_header | semmle.label | ControlFlowNode for rfs_header |
| flask_tests.py:49:44:49:50 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
| flask_tests.py:49:72:49:78 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
| flask_tests.py:49:72:49:97 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript |
| flask_tests.py:53:18:53:24 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
| flask_tests.py:54:41:54:47 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
| flask_tests.py:54:41:54:66 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript |
| flask_tests.py:59:18:59:24 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
| flask_tests.py:60:36:60:42 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
| flask_tests.py:60:36:60:61 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript |
subpaths
#select
| flask_tests.py:13:17:13:26 | ControlFlowNode for rfs_header | flask_tests.py:1:29:1:35 | ControlFlowNode for ImportMember | flask_tests.py:13:17:13:26 | ControlFlowNode for rfs_header | This HTTP header is constructed from a $@. | flask_tests.py:1:29:1:35 | ControlFlowNode for ImportMember | user-provided value |
| flask_tests.py:23:22:23:31 | ControlFlowNode for rfs_header | flask_tests.py:1:29:1:35 | ControlFlowNode for ImportMember | flask_tests.py:23:22:23:31 | ControlFlowNode for rfs_header | This HTTP header is constructed from a $@. | flask_tests.py:1:29:1:35 | ControlFlowNode for ImportMember | user-provided value |
| flask_tests.py:32:22:32:31 | ControlFlowNode for rfs_header | flask_tests.py:1:29:1:35 | ControlFlowNode for ImportMember | flask_tests.py:32:22:32:31 | ControlFlowNode for rfs_header | This HTTP header is constructed from a $@. | flask_tests.py:1:29:1:35 | ControlFlowNode for ImportMember | user-provided value |
| flask_tests.py:43:10:43:19 | ControlFlowNode for rfs_header | flask_tests.py:1:29:1:35 | ControlFlowNode for ImportMember | flask_tests.py:43:10:43:19 | ControlFlowNode for rfs_header | This HTTP header is constructed from a $@. | flask_tests.py:1:29:1:35 | ControlFlowNode for ImportMember | user-provided value |
| flask_tests.py:49:72:49:97 | ControlFlowNode for Subscript | flask_tests.py:1:29:1:35 | ControlFlowNode for ImportMember | flask_tests.py:49:72:49:97 | ControlFlowNode for Subscript | This HTTP header is constructed from a $@. | flask_tests.py:1:29:1:35 | ControlFlowNode for ImportMember | user-provided value |
| flask_tests.py:54:41:54:66 | ControlFlowNode for Subscript | flask_tests.py:1:29:1:35 | ControlFlowNode for ImportMember | flask_tests.py:54:41:54:66 | ControlFlowNode for Subscript | This HTTP header is constructed from a $@. | flask_tests.py:1:29:1:35 | ControlFlowNode for ImportMember | user-provided value |
| flask_tests.py:60:36:60:61 | ControlFlowNode for Subscript | flask_tests.py:1:29:1:35 | ControlFlowNode for ImportMember | flask_tests.py:60:36:60:61 | ControlFlowNode for Subscript | This HTTP header is constructed from a $@. | flask_tests.py:1:29:1:35 | ControlFlowNode for ImportMember | user-provided value |

View File

@@ -40,10 +40,22 @@ def flask_make_response_extend():
resp.headers.extend(
{'HeaderName': rfs_header}) # GOOD
resp.headers.extend(
{rfs_header: "HeaderValue"}) # BAD but not yet found
{rfs_header: "HeaderValue"}) # BAD
return resp
@app.route("/Response_arg")
def Response_arg():
return Response(headers={'HeaderName': request.args["rfs_header"], request.args["rfs_header"]: "HeaderValue"}) # BAD but not yet found
return Response(headers={'HeaderName': request.args["rfs_header"], request.args["rfs_header"]: "HeaderValue"}) # BAD
@app.route("/flask_make_response_header_arg3")
def flask_make_response_header_arg3():
rfs_header = request.args["rfs_header"]
resp = make_response("hello", 200, {request.args["rfs_header"]: "HeaderValue"}) # BAD
return resp
@app.route("/flask_make_response_header_arg2")
def flask_make_response_header_arg2():
rfs_header = request.args["rfs_header"]
resp = make_response("hello", {request.args["rfs_header"]: "HeaderValue"}) # BAD
return resp