Python: Add some tricky tests of return in flask route handler

In these cases the `return` might end up creating a new HTTP response, so they
need to be modeled as such.

Initially I created a very naive solution that didn't handle either
tricky_return1 or tricky_return2.

The interaction in tricky_return2/helper highlighted for me that to handle this
properly, due to the fact that the flow is across functions, we either need to
use a global dataflow/taint-tracking configuration, or some clever use of
type-trackers.

In the end, this extra effort for not modeling all returns in a flask route
handler as a creation of a HTTP response doesn't really seem to be worth it (at
least not right now). Sicne we use it with taint-tracking for the Reflected XSS
query, and use a HTTP response _creation_ as the sink (without propagating taint
to the HTTP response), we won't get into trouble where we report a path to BOTH
`make_response(...)` and the `return`

```
resp = make_response(...)
return resp
```

If we change this setup in the future, we will probably need to do something to
avoid this double-path reporting.
This commit is contained in:
Rasmus Wriedt Larsen
2020-10-22 18:09:30 +02:00
parent d60221b168
commit d2cfa91155
2 changed files with 41 additions and 66 deletions

View File

@@ -1,48 +0,0 @@
| response_test.py:19:12:19:15 | ControlFlowNode for resp | Unexpected result: HttpResponse= |
| response_test.py:19:12:19:15 | ControlFlowNode for resp | Unexpected result: mimetype=text/html |
| response_test.py:19:12:19:15 | ControlFlowNode for resp | Unexpected result: responseBody=resp |
| response_test.py:25:12:25:15 | ControlFlowNode for resp | Unexpected result: HttpResponse= |
| response_test.py:25:12:25:15 | ControlFlowNode for resp | Unexpected result: mimetype=text/html |
| response_test.py:25:12:25:15 | ControlFlowNode for resp | Unexpected result: responseBody=resp |
| response_test.py:35:12:35:15 | ControlFlowNode for resp | Unexpected result: HttpResponse= |
| response_test.py:35:12:35:15 | ControlFlowNode for resp | Unexpected result: mimetype=text/html |
| response_test.py:35:12:35:15 | ControlFlowNode for resp | Unexpected result: responseBody=resp |
| response_test.py:43:12:43:15 | ControlFlowNode for resp | Unexpected result: HttpResponse= |
| response_test.py:43:12:43:15 | ControlFlowNode for resp | Unexpected result: mimetype=text/html |
| response_test.py:43:12:43:15 | ControlFlowNode for resp | Unexpected result: responseBody=resp |
| response_test.py:51:12:51:15 | ControlFlowNode for resp | Unexpected result: HttpResponse= |
| response_test.py:51:12:51:15 | ControlFlowNode for resp | Unexpected result: mimetype=text/html |
| response_test.py:51:12:51:15 | ControlFlowNode for resp | Unexpected result: responseBody=resp |
| response_test.py:58:12:58:19 | ControlFlowNode for response | Unexpected result: HttpResponse= |
| response_test.py:58:12:58:19 | ControlFlowNode for response | Unexpected result: mimetype=text/html |
| response_test.py:58:12:58:19 | ControlFlowNode for response | Unexpected result: responseBody=response |
| response_test.py:70:12:70:15 | ControlFlowNode for resp | Unexpected result: HttpResponse= |
| response_test.py:70:12:70:15 | ControlFlowNode for resp | Unexpected result: mimetype=text/html |
| response_test.py:70:12:70:15 | ControlFlowNode for resp | Unexpected result: responseBody=resp |
| response_test.py:77:12:77:15 | ControlFlowNode for resp | Unexpected result: HttpResponse= |
| response_test.py:77:12:77:15 | ControlFlowNode for resp | Unexpected result: mimetype=text/html |
| response_test.py:77:12:77:15 | ControlFlowNode for resp | Unexpected result: responseBody=resp |
| response_test.py:87:12:87:15 | ControlFlowNode for resp | Unexpected result: HttpResponse= |
| response_test.py:87:12:87:15 | ControlFlowNode for resp | Unexpected result: mimetype=text/html |
| response_test.py:87:12:87:15 | ControlFlowNode for resp | Unexpected result: responseBody=resp |
| response_test.py:93:12:93:15 | ControlFlowNode for resp | Unexpected result: HttpResponse= |
| response_test.py:93:12:93:15 | ControlFlowNode for resp | Unexpected result: mimetype=text/html |
| response_test.py:93:12:93:15 | ControlFlowNode for resp | Unexpected result: responseBody=resp |
| response_test.py:100:12:100:15 | ControlFlowNode for resp | Unexpected result: HttpResponse= |
| response_test.py:100:12:100:15 | ControlFlowNode for resp | Unexpected result: mimetype=text/html |
| response_test.py:100:12:100:15 | ControlFlowNode for resp | Unexpected result: responseBody=resp |
| response_test.py:107:12:107:15 | ControlFlowNode for resp | Unexpected result: HttpResponse= |
| response_test.py:107:12:107:15 | ControlFlowNode for resp | Unexpected result: mimetype=text/html |
| response_test.py:107:12:107:15 | ControlFlowNode for resp | Unexpected result: responseBody=resp |
| response_test.py:115:12:115:15 | ControlFlowNode for resp | Unexpected result: HttpResponse= |
| response_test.py:115:12:115:15 | ControlFlowNode for resp | Unexpected result: mimetype=text/html |
| response_test.py:115:12:115:15 | ControlFlowNode for resp | Unexpected result: responseBody=resp |
| response_test.py:123:12:123:15 | ControlFlowNode for resp | Unexpected result: HttpResponse= |
| response_test.py:123:12:123:15 | ControlFlowNode for resp | Unexpected result: mimetype=text/html |
| response_test.py:123:12:123:15 | ControlFlowNode for resp | Unexpected result: responseBody=resp |
| response_test.py:131:12:131:15 | ControlFlowNode for resp | Unexpected result: HttpResponse= |
| response_test.py:131:12:131:15 | ControlFlowNode for resp | Unexpected result: mimetype=text/html |
| response_test.py:131:12:131:15 | ControlFlowNode for resp | Unexpected result: responseBody=resp |
| response_test.py:139:12:139:15 | ControlFlowNode for resp | Unexpected result: HttpResponse= |
| response_test.py:139:12:139:15 | ControlFlowNode for resp | Unexpected result: mimetype=text/html |
| response_test.py:139:12:139:15 | ControlFlowNode for resp | Unexpected result: responseBody=resp |

View File

@@ -1,6 +1,6 @@
import json
from flask import Flask, make_response, jsonify, Response
from flask import Flask, make_response, jsonify, Response, request
app = Flask(__name__)
@@ -16,13 +16,13 @@ def html2(): # $routeHandler
# show that we recognize the response creation, and not the return (hopefully). (and
# do the same in the following of the file)
resp = make_response("<h1>hello</h1>") # $HttpResponse $mimetype=text/html $responseBody="<h1>hello</h1>"
return resp
return resp # $f+:HttpResponse $f+:mimetype=text/html $f+:responseBody=resp
@app.route("/html3") # $routeSetup="/html3"
def html3(): # $routeHandler
resp = app.make_response("<h1>hello</h1>") # $HttpResponse $mimetype=text/html $responseBody="<h1>hello</h1>"
return resp
return resp # $f+:HttpResponse $f+:mimetype=text/html $f+:responseBody=resp
# TODO: Create test-cases for the many ways that `make_response` can be used
@@ -32,7 +32,7 @@ def html3(): # $routeHandler
@app.route("/html4") # $routeSetup="/html4"
def html4(): # $routeHandler
resp = Response("<h1>hello</h1>") # $HttpResponse $mimetype=text/html $responseBody="<h1>hello</h1>"
return resp
return resp # $f+:HttpResponse $f+:mimetype=text/html $f+:responseBody=resp
@app.route("/html5") # $routeSetup="/html5"
@@ -40,7 +40,7 @@ def html5(): # $routeHandler
# note: flask.Flask.response_class is set to `flask.Response` by default.
# it can be overridden, but we don't try to handle that right now.
resp = Flask.response_class("<h1>hello</h1>") # $HttpResponse $mimetype=text/html $responseBody="<h1>hello</h1>"
return resp
return resp # $f+:HttpResponse $f+:mimetype=text/html $f+:responseBody=resp
@app.route("/html6") # $routeSetup="/html6"
@@ -48,14 +48,37 @@ def html6(): # $routeHandler
# note: app.response_class (flask.Flask.response_class) is set to `flask.Response` by default.
# it can be overridden, but we don't try to handle that right now.
resp = app.response_class("<h1>hello</h1>") # $HttpResponse $mimetype=text/html $responseBody="<h1>hello</h1>"
return resp
return resp # $f+:HttpResponse $f+:mimetype=text/html $f+:responseBody=resp
@app.route("/jsonify") # $routeSetup="/jsonify"
def jsonify_route(): # $routeHandler
data = {"foo": "bar"}
response = jsonify(data) # $f-:HttpResponse $f-:mimetype=application/json $f-:responseBody=data
return response
resp = jsonify(data) # $f-:HttpResponse $f-:mimetype=application/json $f-:responseBody=data
return resp # $f+:HttpResponse $f+:mimetype=text/html $f+:responseBody=resp
################################################################################
# Tricky return handling
################################################################################
@app.route("/tricky-return1") # $routeSetup="/tricky-return1"
def tricky_return1(): # $routeHandler
if "raw" in request.args:
resp = "<h1>hellu</h1>"
else:
resp = make_response("<h1>hello</h1>") # $HttpResponse $mimetype=text/html $responseBody="<h1>hello</h1>"
return resp # $HttpResponse $mimetype=text/html $responseBody=resp
def helper():
if "raw" in request.args:
return "<h1>hellu</h1>"
else:
return make_response("<h1>hello</h1>") # $HttpResponse $mimetype=text/html $responseBody="<h1>hello</h1>"
@app.route("/tricky-return2") # $routeSetup="/tricky-return2"
def tricky_return2(): # $routeHandler
resp = helper()
return resp # $HttpResponse $mimetype=text/html $responseBody=resp
################################################################################
@@ -67,14 +90,14 @@ def jsonify_route(): # $routeHandler
def response_modification1(): # $routeHandler
resp = make_response("<h1>hello</h1>") # $HttpResponse $mimetype=text/html $responseBody="<h1>hello</h1>"
resp.content_type = "text/plain" # $f-:HttpResponse $f-:mimetype=text/plain
return resp
return resp # $f+:HttpResponse $f+:mimetype=text/html $f+:responseBody=resp
@app.route("/content-type/response-modification2") # $routeSetup="/content-type/response-modification2"
def response_modification2(): # $routeHandler
resp = make_response("<h1>hello</h1>") # $HttpResponse $mimetype=text/html $responseBody="<h1>hello</h1>"
resp.headers["content-type"] = "text/plain" # $f-:HttpResponse $f-:mimetype=text/plain
return resp
return resp # $f+:HttpResponse $f+:mimetype=text/html $f+:responseBody=resp
# Exploration of mimetype/content_type/headers arguments to `app.response_class` -- things can get tricky
@@ -84,27 +107,27 @@ def response_modification2(): # $routeHandler
@app.route("/content-type/Response1") # $routeSetup="/content-type/Response1"
def Response1(): # $routeHandler
resp = Response("<h1>hello</h1>", mimetype="text/plain") # $HttpResponse $mimetype=text/plain $responseBody="<h1>hello</h1>"
return resp
return resp # $f+:HttpResponse $f+:mimetype=text/html $f+:responseBody=resp
@app.route("/content-type/Response2") # $routeSetup="/content-type/Response2"
def Response2(): # $routeHandler
resp = Response("<h1>hello</h1>", content_type="text/plain; charset=utf-8") # $HttpResponse $mimetype=text/plain $responseBody="<h1>hello</h1>"
return resp
return resp # $f+:HttpResponse $f+:mimetype=text/html $f+:responseBody=resp
@app.route("/content-type/Response3") # $routeSetup="/content-type/Response3"
def Response3(): # $routeHandler
# content_type argument takes priority (and result is text/plain)
resp = Response("<h1>hello</h1>", content_type="text/plain; charset=utf-8", mimetype="text/html") # $HttpResponse $mimetype=text/plain $responseBody="<h1>hello</h1>"
return resp
return resp # $f+:HttpResponse $f+:mimetype=text/html $f+:responseBody=resp
@app.route("/content-type/Response4") # $routeSetup="/content-type/Response4"
def Response4(): # $routeHandler
# note: capitalization of Content-Type does not matter
resp = Response("<h1>hello</h1>", headers={"Content-TYPE": "text/plain"}) # $HttpResponse $f+:mimetype=text/html $f-:mimetype=text/plain $responseBody="<h1>hello</h1>"
return resp
return resp # $f+:HttpResponse $f+:mimetype=text/html $f+:responseBody=resp
@app.route("/content-type/Response5") # $routeSetup="/content-type/Response5"
@@ -112,7 +135,7 @@ def Response5(): # $routeHandler
# content_type argument takes priority (and result is text/plain)
# note: capitalization of Content-Type does not matter
resp = Response("<h1>hello</h1>", headers={"Content-TYPE": "text/html"}, content_type="text/plain; charset=utf-8") # $HttpResponse $mimetype=text/plain $responseBody="<h1>hello</h1>"
return resp
return resp # $f+:HttpResponse $f+:mimetype=text/html $f+:responseBody=resp
@app.route("/content-type/Response6") # $routeSetup="/content-type/Response6"
@@ -120,7 +143,7 @@ def Response6(): # $routeHandler
# mimetype argument takes priority over header (and result is text/plain)
# note: capitalization of Content-Type does not matter
resp = Response("<h1>hello</h1>", headers={"Content-TYPE": "text/html"}, mimetype="text/plain") # $HttpResponse $mimetype=text/plain $responseBody="<h1>hello</h1>"
return resp
return resp # $f+:HttpResponse $f+:mimetype=text/html $f+:responseBody=resp
@app.route("/content-type/Flask-response-class") # $routeSetup="/content-type/Flask-response-class"
@@ -128,7 +151,7 @@ def Flask_response_class(): # $routeHandler
# note: flask.Flask.response_class is set to `flask.Response` by default.
# it can be overridden, but we don't try to handle that right now.
resp = Flask.response_class("<h1>hello</h1>", mimetype="text/plain") # $HttpResponse $mimetype=text/plain $responseBody="<h1>hello</h1>"
return resp
return resp # $f+:HttpResponse $f+:mimetype=text/html $f+:responseBody=resp
@app.route("/content-type/app-response-class") # $routeSetup="/content-type/app-response-class"
@@ -136,7 +159,7 @@ def app_response_class(): # $routeHandler
# note: app.response_class (flask.Flask.response_class) is set to `flask.Response` by default.
# it can be overridden, but we don't try to handle that right now.
resp = app.response_class("<h1>hello</h1>", mimetype="text/plain") # $HttpResponse $mimetype=text/plain $responseBody="<h1>hello</h1>"
return resp
return resp # $f+:HttpResponse $f+:mimetype=text/html $f+:responseBody=resp
# TODO: add tests for setting status code