Skip to content

Commit 74fada6

Browse files
committed
Skip JSON parsing if Content-Type is mismatched
1 parent f1ae764 commit 74fada6

15 files changed

+114
-0
lines changed

CHANGELOG.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
Changelog
22
---------
33

4+
5.5.3 (unreleased)
5+
******************
6+
7+
Bug fixes:
8+
9+
* :cve:`CVE-2020-7965`: Don't attempt to parse JSON if the request's Content-Type is mismatched.
10+
411
5.5.2 (2019-10-06)
512
******************
613

src/webargs/bottleparser.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@ def parse_querystring(self, req, name, field):
3232

3333
def parse_form(self, req, name, field):
3434
"""Pull a form value from the request."""
35+
# For consistency with other parsers' behavior, don't attempt to
36+
# parse if content-type is mismatched.
37+
# TODO: Make this check more specific
38+
if core.is_json(req.content_type):
39+
return core.missing
3540
return core.get_value(req.forms, name, field)
3641

3742
def parse_json(self, req, name, field):

src/webargs/djangoparser.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ def parse_json(self, req, name, field):
4545
"""Pull a json value from the request body."""
4646
json_data = self._cache.get("json")
4747
if json_data is None:
48+
if not core.is_json(req.content_type):
49+
return core.missing
50+
4851
try:
4952
self._cache["json"] = json_data = core.parse_json(req.body)
5053
except AttributeError:

src/webargs/flaskparser.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@ def parse_json(self, req, name, field):
6161
"""Pull a json value from the request."""
6262
json_data = self._cache.get("json")
6363
if json_data is None:
64+
if not is_json_request(req):
65+
return core.missing
66+
6467
# We decode the json manually here instead of
6568
# using req.get_json() so that we can handle
6669
# JSONDecodeErrors consistently

src/webargs/pyramidparser.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ def parse_json(self, req, name, field):
5757
"""Pull a json value from the request."""
5858
json_data = self._cache.get("json")
5959
if json_data is None:
60+
if not core.is_json(req.content_type):
61+
return core.missing
62+
6063
try:
6164
self._cache["json"] = json_data = core.parse_json(req.body, req.charset)
6265
except json.JSONDecodeError as e:

src/webargs/testing.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,3 +217,18 @@ def test_invalid_json(self, testapp):
217217
)
218218
assert res.status_code == 400
219219
assert res.json == {"json": ["Invalid JSON body."]}
220+
221+
@pytest.mark.parametrize(
222+
("path", "payload", "content_type"),
223+
[
224+
(
225+
"/echo_json",
226+
json.dumps({"name": "foo"}),
227+
"application/x-www-form-urlencoded",
228+
),
229+
("/echo_form", {"name": "foo"}, "application/json"),
230+
],
231+
)
232+
def test_content_type_mismatch(self, testapp, path, payload, content_type):
233+
res = testapp.post(path, payload, headers={"Content-Type": content_type})
234+
assert res.json == {"name": "World"}

src/webargs/webapp2parser.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ def parse_json(self, req, name, field):
4141
"""Pull a json value from the request."""
4242
json_data = self._cache.get("json")
4343
if json_data is None:
44+
if not core.is_json(req.content_type):
45+
return core.missing
46+
4447
try:
4548
self._cache["json"] = json_data = core.parse_json(req.body)
4649
except json.JSONDecodeError as e:

tests/apps/aiohttp_app.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,16 @@ async def echo_query(request):
4444
return json_response(parsed)
4545

4646

47+
async def echo_json(request):
48+
parsed = await parser.parse(hello_args, request, locations=("json",))
49+
return json_response(parsed)
50+
51+
52+
async def echo_form(request):
53+
parsed = await parser.parse(hello_args, request, locations=("form",))
54+
return json_response(parsed)
55+
56+
4757
@use_args(hello_args)
4858
async def echo_use_args(request, args):
4959
return json_response(args)
@@ -170,6 +180,8 @@ def create_app():
170180

171181
add_route(app, ["GET", "POST"], "/echo", echo)
172182
add_route(app, ["GET"], "/echo_query", echo_query)
183+
add_route(app, ["POST"], "/echo_json", echo_json)
184+
add_route(app, ["POST"], "/echo_form", echo_form)
173185
add_route(app, ["GET", "POST"], "/echo_use_args", echo_use_args)
174186
add_route(app, ["GET", "POST"], "/echo_use_kwargs", echo_use_kwargs)
175187
add_route(app, ["GET", "POST"], "/echo_use_args_validated", echo_use_args_validated)

tests/apps/bottle_app.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,16 @@ def echo_query():
3232
return parser.parse(hello_args, request, locations=("query",))
3333

3434

35+
@app.route("/echo_json", method=["POST"])
36+
def echo_json():
37+
return parser.parse(hello_args, request, locations=("json",))
38+
39+
40+
@app.route("/echo_form", method=["POST"])
41+
def echo_form():
42+
return parser.parse(hello_args, request, locations=("form",))
43+
44+
3545
@app.route("/echo_use_args", method=["GET", "POST"])
3646
@use_args(hello_args)
3747
def echo_use_args(args):

tests/apps/django_app/base/urls.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
urlpatterns = [
66
url(r"^echo$", views.echo),
77
url(r"^echo_query$", views.echo_query),
8+
url(r"^echo_json$", views.echo_json),
9+
url(r"^echo_form$", views.echo_form),
810
url(r"^echo_use_args$", views.echo_use_args),
911
url(r"^echo_use_kwargs$", views.echo_use_kwargs),
1012
url(r"^echo_multi$", views.echo_multi),

tests/apps/django_app/echo/views.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,14 @@ def echo_query(request):
3737
return json_response(parser.parse(hello_args, request, locations=("query",)))
3838

3939

40+
def echo_json(request):
41+
return json_response(parser.parse(hello_args, request, locations=("json",)))
42+
43+
44+
def echo_form(request):
45+
return json_response(parser.parse(hello_args, request, locations=("form",)))
46+
47+
4048
@use_args(hello_args)
4149
def echo_use_args(request, args):
4250
return json_response(args)

tests/apps/falcon_app.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,18 @@ def on_get(self, req, resp):
3737
resp.body = json.dumps(parsed)
3838

3939

40+
class EchoJSON(object):
41+
def on_post(self, req, resp):
42+
parsed = parser.parse(hello_args, req, locations=("json",))
43+
resp.body = json.dumps(parsed)
44+
45+
46+
class EchoForm(object):
47+
def on_post(self, req, resp):
48+
parsed = parser.parse(hello_args, req, locations=("form",))
49+
resp.body = json.dumps(parsed)
50+
51+
4052
class EchoUseArgs(object):
4153
@use_args(hello_args)
4254
def on_get(self, req, resp, args):
@@ -144,6 +156,8 @@ def create_app():
144156
app = falcon.API()
145157
app.add_route("/echo", Echo())
146158
app.add_route("/echo_query", EchoQuery())
159+
app.add_route("/echo_json", EchoJSON())
160+
app.add_route("/echo_form", EchoForm())
147161
app.add_route("/echo_use_args", EchoUseArgs())
148162
app.add_route("/echo_use_kwargs", EchoUseKwargs())
149163
app.add_route("/echo_use_args_validated", EchoUseArgsValidated())

tests/apps/flask_app.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,16 @@ def echo_query():
3737
return J(parser.parse(hello_args, request, locations=("query",)))
3838

3939

40+
@app.route("/echo_json", methods=["POST"])
41+
def echo_json():
42+
return J(parser.parse(hello_args, request, locations=("json",)))
43+
44+
45+
@app.route("/echo_form", methods=["POST"])
46+
def echo_form():
47+
return J(parser.parse(hello_args, request, locations=("form",)))
48+
49+
4050
@app.route("/echo_use_args", methods=["GET", "POST"])
4151
@use_args(hello_args)
4252
def echo_use_args(args):

tests/apps/pyramid_app.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,14 @@ def echo_query(request):
3434
return parser.parse(hello_args, request, locations=("query",))
3535

3636

37+
def echo_json(request):
38+
return parser.parse(hello_args, request, locations=("json",))
39+
40+
41+
def echo_form(request):
42+
return parser.parse(hello_args, request, locations=("form",))
43+
44+
3745
@use_args(hello_args)
3846
def echo_use_args(request, args):
3947
return args
@@ -128,6 +136,8 @@ def create_app():
128136

129137
add_route(config, "/echo", echo)
130138
add_route(config, "/echo_query", echo_query)
139+
add_route(config, "/echo_json", echo_json)
140+
add_route(config, "/echo_form", echo_form)
131141
add_route(config, "/echo_use_args", echo_use_args)
132142
add_route(config, "/echo_use_args_validated", echo_use_args_validated)
133143
add_route(config, "/echo_use_kwargs", echo_use_kwargs)

tests/test_webapp2parser.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,15 @@ def test_parse_json():
6161
assert parser.parse(hello_args, req=request) == expected
6262

6363

64+
def test_parse_json_content_type_mismatch():
65+
request = webapp2.Request.blank(
66+
"/echo_json",
67+
POST=json.dumps({"name": "foo"}),
68+
headers={"content-type": "application/x-www-form-urlencoded"},
69+
)
70+
assert parser.parse(hello_args, req=request) == {"name": "World"}
71+
72+
6473
def test_parse_invalid_json():
6574
request = webapp2.Request.blank(
6675
"/echo", POST='{"foo": "bar", }', headers={"content-type": "application/json"}

0 commit comments

Comments
 (0)