-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added flask-json-response-type codemod
- Loading branch information
1 parent
235516b
commit 839d82b
Showing
8 changed files
with
205 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
from core_codemods.flask_json_response_type import FlaskJsonResponseType | ||
from integration_tests.base_test import ( | ||
BaseIntegrationTest, | ||
original_and_expected_from_code_path, | ||
) | ||
|
||
|
||
class TestFlaskJsonResponseType(BaseIntegrationTest): | ||
codemod = FlaskJsonResponseType | ||
code_path = "tests/samples/flask_json_response_type.py" | ||
original_code, expected_new_code = original_and_expected_from_code_path( | ||
code_path, | ||
[ | ||
( | ||
5, | ||
""" return make_response(json_response, mimetype="application/json")\n""", | ||
), | ||
], | ||
) | ||
|
||
# fmt: off | ||
expected_diff =( | ||
"""--- \n""" | ||
"""+++ \n""" | ||
"""@@ -3,4 +3,4 @@\n""" | ||
""" \n""" | ||
""" def foo(request):\n""" | ||
""" json_response = json.dumps({ "user_input": request.GET.get("input") })\n""" | ||
"""- return make_response(json_response)\n""" | ||
"""+ return make_response(json_response, mimetype="application/json")\n""" | ||
) | ||
# fmt: on | ||
|
||
expected_line_change = "6" | ||
change_description = FlaskJsonResponseType.CHANGE_DESCRIPTION | ||
num_changed_files = 1 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
13 changes: 13 additions & 0 deletions
13
src/core_codemods/docs/pixee_python_flask-json-response-type.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
The default `mimetype` for `make_response` in Flask is `'text/html'`. This is true even when the response contains JSON data. | ||
If the JSON contains (unsanitized) user-supplied input, a malicious user may supply HTML code which leaves the application vulnerable to cross-site scripting (XSS). | ||
This fix explicitly sets the response type to `application/json` when the response body is JSON data to avoid this vulnerability. Our changes look something like this: | ||
|
||
```diff | ||
from flask import make_response | ||
import json | ||
|
||
def foo(request): | ||
json_response = json.dumps({ "user_input": request.GET.get("input") }) | ||
- return make_response(json_response) | ||
+ return make_response(json_response, mimetype="application/json") | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
import libcst as cst | ||
|
||
from codemodder.codemods.base_codemod import ReviewGuidance | ||
from codemodder.codemods.api import SemgrepCodemod | ||
|
||
|
||
class FlaskJsonResponseType(SemgrepCodemod): | ||
NAME = "flask-json-response-type" | ||
SUMMARY = "Set content type to `json/application` for `flask.make_response` with JSON data" | ||
REVIEW_GUIDANCE = ReviewGuidance.MERGE_WITHOUT_REVIEW | ||
DESCRIPTION = "Sets `mimetype` to `json/application`." | ||
REFERENCES = [ | ||
{ | ||
"url": "https://tedboy.github.io/flask/generated/flask.jsonify.html", | ||
"description": "", | ||
}, | ||
{ | ||
"url": "https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html#output-encoding-for-javascript-contexts", | ||
"description": "", | ||
}, | ||
] | ||
|
||
@classmethod | ||
def rule(cls): | ||
return """ | ||
rules: | ||
- id: flask-json-response-type | ||
mode: taint | ||
pattern-sources: | ||
- pattern: json.dumps(...) | ||
pattern-sinks: | ||
- patterns: | ||
- pattern: flask.make_response(...) | ||
- pattern-not: flask.make_response(...,mimetype=...,...) | ||
""" | ||
|
||
def on_result_found(self, _, updated_node): | ||
return self.update_arg_target( | ||
updated_node, | ||
[ | ||
*updated_node.args, | ||
cst.Arg( | ||
value=cst.parse_expression('"application/json"'), | ||
keyword=cst.Name("mimetype"), | ||
equal=cst.AssignEqual( | ||
whitespace_before=cst.SimpleWhitespace(""), | ||
whitespace_after=cst.SimpleWhitespace(""), | ||
), | ||
), | ||
], | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
from core_codemods.flask_json_response_type import FlaskJsonResponseType | ||
from tests.codemods.base_codemod_test import BaseSemgrepCodemodTest | ||
from textwrap import dedent | ||
|
||
|
||
class TestFlaskJsonResponseType(BaseSemgrepCodemodTest): | ||
codemod = FlaskJsonResponseType | ||
|
||
def test_name(self): | ||
assert self.codemod.name() == "flask-json-response-type" | ||
|
||
def test_simple(self, tmpdir): | ||
input_code = """\ | ||
from flask import make_response | ||
import json | ||
def foo(request): | ||
json_response = json.dumps({ "user_input": request.GET.get("input") }) | ||
return make_response(json_response) | ||
""" | ||
expected = """\ | ||
from flask import make_response | ||
import json | ||
def foo(request): | ||
json_response = json.dumps({ "user_input": request.GET.get("input") }) | ||
return make_response(json_response, mimetype="application/json") | ||
""" | ||
self.run_and_assert(tmpdir, dedent(input_code), dedent(expected)) | ||
assert len(self.file_context.codemod_changes) == 1 | ||
|
||
def test_alias(self, tmpdir): | ||
input_code = """\ | ||
from flask import make_response as response | ||
import json | ||
def foo(request): | ||
json_response = json.dumps({ "user_input": request.GET.get("input") }) | ||
return response(json_response) | ||
""" | ||
expected = """\ | ||
from flask import make_response as response | ||
import json | ||
def foo(request): | ||
json_response = json.dumps({ "user_input": request.GET.get("input") }) | ||
return response(json_response, mimetype="application/json") | ||
""" | ||
self.run_and_assert(tmpdir, dedent(input_code), dedent(expected)) | ||
assert len(self.file_context.codemod_changes) == 1 | ||
|
||
def test_direct(self, tmpdir): | ||
input_code = """\ | ||
from flask import make_response | ||
import json | ||
def foo(request): | ||
return make_response(json.dumps({ "user_input": request.GET.get("input") })) | ||
""" | ||
expected = """\ | ||
from flask import make_response | ||
import json | ||
def foo(request): | ||
return make_response(json.dumps({ "user_input": request.GET.get("input") }), mimetype="application/json") | ||
""" | ||
self.run_and_assert(tmpdir, dedent(input_code), dedent(expected)) | ||
assert len(self.file_context.codemod_changes) == 1 | ||
|
||
def test_mimetype_set(self, tmpdir): | ||
input_code = """\ | ||
from flask import make_response | ||
import json | ||
def foo(request): | ||
json_response = json.dumps({ "user_input": request.GET.get("input") }) | ||
return make_response(json_response, mimetype='application/json') | ||
""" | ||
self.run_and_assert(tmpdir, dedent(input_code), dedent(input_code)) | ||
assert len(self.file_context.codemod_changes) == 0 | ||
|
||
def test_no_json_input(self, tmpdir): | ||
input_code = """\ | ||
from flask import make_response | ||
import json | ||
def foo(request): | ||
dict_response = { "user_input": request.GET.get("input") } | ||
return make_response(dict_response) | ||
""" | ||
self.run_and_assert(tmpdir, dedent(input_code), dedent(input_code)) | ||
assert len(self.file_context.codemod_changes) == 0 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
from flask import make_response | ||
import json | ||
|
||
def foo(request): | ||
json_response = json.dumps({ "user_input": request.GET.get("input") }) | ||
return make_response(json_response) |