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:
parent
5e28cd3f2c
commit
1c6e8ec4ed
@ -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
|
||||||
header_end = chunk.find(b"\r\n\r\n")
|
while True:
|
||||||
|
if headers_chunk_size > max_header_size:
|
||||||
|
raise MultiPartParserError("Request max total header size exceeded.")
|
||||||
|
|
||||||
if header_end == -1:
|
# Stream at beginning of header, look for end of header and parse it if
|
||||||
# we find no header, so we just mark this fact and pass on
|
# found. The header must fit within one chunk.
|
||||||
# the stream verbatim
|
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")
|
||||||
|
if header_end != -1:
|
||||||
|
break
|
||||||
|
|
||||||
|
# Find no header, mark this fact and pass on the stream verbatim.
|
||||||
stream.unget(chunk)
|
stream.unget(chunk)
|
||||||
return (RAW, {}, stream)
|
# No more data to read.
|
||||||
|
if len(chunk) < headers_chunk_size:
|
||||||
|
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)
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user