1
0
mirror of https://github.com/django/django.git synced 2024-12-23 01:25:58 +00:00

Fixed #34968 -- Made multipart parsing of headers raise an error on too long headers.

This also allow customizing the maximum size of headers via
MAX_TOTAL_HEADER_SIZE.
This commit is contained in:
Standa Opichal 2023-11-10 17:40:24 +01:00 committed by Mariusz Felisiak
parent 5e28cd3f2c
commit 1c6e8ec4ed
3 changed files with 101 additions and 14 deletions

View File

@ -41,6 +41,7 @@ RAW = "raw"
FILE = "file" FILE = "file"
FIELD = "field" FIELD = "field"
FIELD_TYPES = frozenset([FIELD, RAW]) FIELD_TYPES = frozenset([FIELD, RAW])
MAX_TOTAL_HEADER_SIZE = 1024
class MultiPartParser: class MultiPartParser:
@ -682,21 +683,30 @@ def parse_boundary_stream(stream, max_header_size):
""" """
Parse one and exactly one stream that encapsulates a boundary. Parse one and exactly one stream that encapsulates a boundary.
""" """
# Stream at beginning of header, look for end of header
# and parse it if found. The header must fit within one
# chunk.
chunk = stream.read(max_header_size)
# 'find' returns the top of these four bytes, so we'll # Look for the end of headers and if not found extend the search to double
# need to munch them later to prevent them from polluting # the size up to the MAX_TOTAL_HEADER_SIZE.
# the payload. headers_chunk_size = 1024
while True:
if headers_chunk_size > max_header_size:
raise MultiPartParserError("Request max total header size exceeded.")
# Stream at beginning of header, look for end of header and parse it if
# found. The header must fit within one chunk.
chunk = stream.read(headers_chunk_size)
# 'find' returns the top of these four bytes, so munch them later to
# prevent them from polluting the payload.
header_end = chunk.find(b"\r\n\r\n") header_end = chunk.find(b"\r\n\r\n")
if header_end != -1:
break
if header_end == -1: # Find no header, mark this fact and pass on the stream verbatim.
# we find no header, so we just mark this fact and pass on
# the stream verbatim
stream.unget(chunk) stream.unget(chunk)
# No more data to read.
if len(chunk) < headers_chunk_size:
return (RAW, {}, stream) return (RAW, {}, stream)
# Double the chunk size.
headers_chunk_size *= 2
header = chunk[:header_end] header = chunk[:header_end]
@ -740,4 +750,4 @@ class Parser:
boundarystream = InterBoundaryIter(self._stream, self._separator) boundarystream = InterBoundaryIter(self._stream, self._separator)
for sub_stream in boundarystream: for sub_stream in boundarystream:
# Iterate over each part # Iterate over each part
yield parse_boundary_stream(sub_stream, 1024) yield parse_boundary_stream(sub_stream, MAX_TOTAL_HEADER_SIZE)

View File

@ -16,6 +16,7 @@ from django.core.files.storage import default_storage
from django.core.files.uploadedfile import SimpleUploadedFile, UploadedFile from django.core.files.uploadedfile import SimpleUploadedFile, UploadedFile
from django.http.multipartparser import ( from django.http.multipartparser import (
FILE, FILE,
MAX_TOTAL_HEADER_SIZE,
MultiPartParser, MultiPartParser,
MultiPartParserError, MultiPartParserError,
Parser, Parser,
@ -603,6 +604,57 @@ class FileUploadTests(TestCase):
temp_path = response.json()["temp_path"] temp_path = response.json()["temp_path"]
self.assertIs(os.path.exists(temp_path), False) self.assertIs(os.path.exists(temp_path), False)
def test_upload_large_header_fields(self):
payload = client.FakePayload(
"\r\n".join(
[
"--" + client.BOUNDARY,
'Content-Disposition: form-data; name="my_file"; '
'filename="test.txt"',
"Content-Type: text/plain",
"X-Long-Header: %s" % ("-" * 500),
"",
"file contents",
"--" + client.BOUNDARY + "--\r\n",
]
),
)
r = {
"CONTENT_LENGTH": len(payload),
"CONTENT_TYPE": client.MULTIPART_CONTENT,
"PATH_INFO": "/echo_content/",
"REQUEST_METHOD": "POST",
"wsgi.input": payload,
}
response = self.client.request(**r)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), {"my_file": "file contents"})
def test_upload_header_fields_too_large(self):
payload = client.FakePayload(
"\r\n".join(
[
"--" + client.BOUNDARY,
'Content-Disposition: form-data; name="my_file"; '
'filename="test.txt"',
"Content-Type: text/plain",
"X-Long-Header: %s" % ("-" * (MAX_TOTAL_HEADER_SIZE + 1)),
"",
"file contents",
"--" + client.BOUNDARY + "--\r\n",
]
),
)
r = {
"CONTENT_LENGTH": len(payload),
"CONTENT_TYPE": client.MULTIPART_CONTENT,
"PATH_INFO": "/echo_content/",
"REQUEST_METHOD": "POST",
"wsgi.input": payload,
}
response = self.client.request(**r)
self.assertEqual(response.status_code, 400)
def test_fileupload_getlist(self): def test_fileupload_getlist(self):
file = tempfile.NamedTemporaryFile file = tempfile.NamedTemporaryFile
with file() as file1, file() as file2, file() as file2a: with file() as file1, file() as file2, file() as file2a:

View File

@ -11,7 +11,7 @@ from django.http import (
RawPostDataException, RawPostDataException,
UnreadablePostError, UnreadablePostError,
) )
from django.http.multipartparser import MultiPartParserError from django.http.multipartparser import MAX_TOTAL_HEADER_SIZE, MultiPartParserError
from django.http.request import split_domain_port from django.http.request import split_domain_port
from django.test import RequestFactory, SimpleTestCase, override_settings from django.test import RequestFactory, SimpleTestCase, override_settings
from django.test.client import BOUNDARY, MULTIPART_CONTENT, FakePayload from django.test.client import BOUNDARY, MULTIPART_CONTENT, FakePayload
@ -691,6 +691,31 @@ class RequestsTests(SimpleTestCase):
with self.assertRaisesMessage(MultiPartParserError, msg): with self.assertRaisesMessage(MultiPartParserError, msg):
request.POST request.POST
def test_multipart_with_header_fields_too_large(self):
payload = FakePayload(
"\r\n".join(
[
"--boundary",
'Content-Disposition: form-data; name="name"',
"X-Long-Header: %s" % ("-" * (MAX_TOTAL_HEADER_SIZE + 1)),
"",
"value",
"--boundary--",
]
)
)
request = WSGIRequest(
{
"REQUEST_METHOD": "POST",
"CONTENT_TYPE": "multipart/form-data; boundary=boundary",
"CONTENT_LENGTH": len(payload),
"wsgi.input": payload,
}
)
msg = "Request max total header size exceeded."
with self.assertRaisesMessage(MultiPartParserError, msg):
request.POST
def test_POST_connection_error(self): def test_POST_connection_error(self):
""" """
If wsgi.input.read() raises an exception while trying to read() the If wsgi.input.read() raises an exception while trying to read() the