decorator: Only URL-decode application/x-www-form-urlencoded requests.

We previously parsed any request with method other than {GET, POST} and
Content-Type other than multipart/form-data as if it were
application/x-www-form-urlencoded.

Check that Content-Type is application/x-www-form-urlencoded before
parsing the body that way.  Restrict this logic to {DELETE, PATCH,
PUT} (having a body at all doesn’t make sense for {CONNECT, HEAD,
OPTIONS, TRACE}).

Signed-off-by: Anders Kaseorg <anders@zulip.com>
This commit is contained in:
Anders Kaseorg 2022-06-23 20:18:42 -07:00 committed by Tim Abbott
parent 1ff30bba7c
commit c1a54f4567
4 changed files with 12 additions and 8 deletions

View File

@ -728,7 +728,7 @@ def process_as_post(view_func: ViewFuncT) -> ViewFuncT:
request.upload_handlers, request.upload_handlers,
request.encoding, request.encoding,
).parse() ).parse()
else: elif request.content_type == "application/x-www-form-urlencoded":
request.POST = QueryDict(request.body, encoding=request.encoding) request.POST = QueryDict(request.body, encoding=request.encoding)
return view_func(request, *args, **kwargs) return view_func(request, *args, **kwargs)

View File

@ -157,10 +157,10 @@ def rest_dispatch(request: HttpRequest, **kwargs: Any) -> HttpResponse:
# will generate the appropriate HTTP response. # will generate the appropriate HTTP response.
raise MissingAuthenticationError() raise MissingAuthenticationError()
if request.method not in ["GET", "POST"]: if request.method in ["DELETE", "PATCH", "PUT"]:
# process_as_post needs to be the outer decorator, because # process_as_post needs to be the outer decorator, because
# otherwise we might access and thus cache a value for # otherwise we might access and thus cache a value for
# request.REQUEST. # request.POST.
target_function = process_as_post(target_function) target_function = process_as_post(target_function)
return target_function(request, **kwargs) return target_function(request, **kwargs)

View File

@ -311,6 +311,7 @@ Output:
We need to urlencode, since Django's function won't do it for us. We need to urlencode, since Django's function won't do it for us.
""" """
encoded = urllib.parse.urlencode(info) encoded = urllib.parse.urlencode(info)
kwargs["content_type"] = "application/x-www-form-urlencoded"
django_client = self.client # see WRAPPER_COMMENT django_client = self.client # see WRAPPER_COMMENT
self.set_http_headers(kwargs) self.set_http_headers(kwargs)
result = django_client.patch(url, encoded, **kwargs) result = django_client.patch(url, encoded, **kwargs)
@ -356,6 +357,7 @@ Output:
self, url: str, info: Dict[str, Any] = {}, **kwargs: ClientArg self, url: str, info: Dict[str, Any] = {}, **kwargs: ClientArg
) -> "TestHttpResponse": ) -> "TestHttpResponse":
encoded = urllib.parse.urlencode(info) encoded = urllib.parse.urlencode(info)
kwargs["content_type"] = "application/x-www-form-urlencoded"
django_client = self.client # see WRAPPER_COMMENT django_client = self.client # see WRAPPER_COMMENT
self.set_http_headers(kwargs) self.set_http_headers(kwargs)
return django_client.put(url, encoded, **kwargs) return django_client.put(url, encoded, **kwargs)
@ -373,6 +375,7 @@ Output:
self, url: str, info: Dict[str, Any] = {}, **kwargs: ClientArg self, url: str, info: Dict[str, Any] = {}, **kwargs: ClientArg
) -> "TestHttpResponse": ) -> "TestHttpResponse":
encoded = urllib.parse.urlencode(info) encoded = urllib.parse.urlencode(info)
kwargs["content_type"] = "application/x-www-form-urlencoded"
django_client = self.client # see WRAPPER_COMMENT django_client = self.client # see WRAPPER_COMMENT
self.set_http_headers(kwargs) self.set_http_headers(kwargs)
result = django_client.delete(url, encoded, **kwargs) result = django_client.delete(url, encoded, **kwargs)
@ -383,19 +386,17 @@ Output:
def client_options( def client_options(
self, url: str, info: Dict[str, Any] = {}, **kwargs: ClientArg self, url: str, info: Dict[str, Any] = {}, **kwargs: ClientArg
) -> "TestHttpResponse": ) -> "TestHttpResponse":
encoded = urllib.parse.urlencode(info)
django_client = self.client # see WRAPPER_COMMENT django_client = self.client # see WRAPPER_COMMENT
self.set_http_headers(kwargs) self.set_http_headers(kwargs)
return django_client.options(url, encoded, **kwargs) return django_client.options(url, info, **kwargs)
@instrument_url @instrument_url
def client_head( def client_head(
self, url: str, info: Dict[str, Any] = {}, **kwargs: ClientArg self, url: str, info: Dict[str, Any] = {}, **kwargs: ClientArg
) -> "TestHttpResponse": ) -> "TestHttpResponse":
encoded = urllib.parse.urlencode(info)
django_client = self.client # see WRAPPER_COMMENT django_client = self.client # see WRAPPER_COMMENT
self.set_http_headers(kwargs) self.set_http_headers(kwargs)
return django_client.head(url, encoded, **kwargs) return django_client.head(url, info, **kwargs)
@instrument_url @instrument_url
def client_post( def client_post(

View File

@ -119,7 +119,10 @@ def generate_all_emails(request: HttpRequest) -> HttpResponse:
# Verification for new email # Verification for new email
result = client.patch( result = client.patch(
"/json/settings", urllib.parse.urlencode({"email": "hamlets-new@zulip.com"}), **host_kwargs "/json/settings",
urllib.parse.urlencode({"email": "hamlets-new@zulip.com"}),
content_type="application/x-www-form-urlencoded",
**host_kwargs,
) )
assert result.status_code == 200 assert result.status_code == 200