mirror of https://github.com/django/django.git
[1.4.x] Prevented views.static.serve() from using large memory on large files.
This is a security fix. Disclosure following shortly.
This commit is contained in:
parent
4c241f1b71
commit
d020da6646
|
@ -16,6 +16,9 @@ from django.template import loader, Template, Context, TemplateDoesNotExist
|
|||
from django.utils.http import http_date, parse_http_date
|
||||
from django.utils.translation import ugettext as _, ugettext_noop
|
||||
|
||||
STREAM_CHUNK_SIZE = 4096
|
||||
|
||||
|
||||
def serve(request, path, document_root=None, show_indexes=False):
|
||||
"""
|
||||
Serve static files below a given point in the directory structure.
|
||||
|
@ -59,8 +62,8 @@ def serve(request, path, document_root=None, show_indexes=False):
|
|||
if not was_modified_since(request.META.get('HTTP_IF_MODIFIED_SINCE'),
|
||||
statobj.st_mtime, statobj.st_size):
|
||||
return HttpResponseNotModified(mimetype=mimetype)
|
||||
with open(fullpath, 'rb') as f:
|
||||
response = HttpResponse(f.read(), mimetype=mimetype)
|
||||
f = open(fullpath, 'rb')
|
||||
response = HttpResponse(iter(lambda: f.read(STREAM_CHUNK_SIZE), ''), mimetype=mimetype)
|
||||
response["Last-Modified"] = http_date(statobj.st_mtime)
|
||||
if stat.S_ISREG(statobj.st_mode):
|
||||
response["Content-Length"] = statobj.st_size
|
||||
|
|
|
@ -45,6 +45,21 @@ from a XSS attack. This bug doesn't affect Django currently, since we only put
|
|||
this URL into the ``Location`` response header and browsers seem to ignore
|
||||
JavaScript there.
|
||||
|
||||
Denial-of-service attack against ``django.views.static.serve``
|
||||
==============================================================
|
||||
|
||||
In older versions of Django, the :func:`django.views.static.serve` view read
|
||||
the files it served one line at a time. Therefore, a big file with no newlines
|
||||
would result in memory usage equal to the size of that file. An attacker could
|
||||
exploit this and launch a denial-of-service attack by simultaneously requesting
|
||||
many large files. This view now reads the file in chunks to prevent large
|
||||
memory usage.
|
||||
|
||||
Note, however, that this view has always carried a warning that it is not
|
||||
hardened for production use and should be used only as a development aid. Now
|
||||
may be a good time to audit your project and serve your files in production
|
||||
using a real front-end web server if you are not doing so.
|
||||
|
||||
Bugfixes
|
||||
========
|
||||
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -7,6 +7,7 @@ from django.conf import settings
|
|||
from django.conf.urls.static import static
|
||||
from django.test import TestCase
|
||||
from django.http import HttpResponseNotModified
|
||||
from django.views.static import STREAM_CHUNK_SIZE
|
||||
|
||||
from .. import urls
|
||||
from ..urls import media_dir
|
||||
|
@ -29,10 +30,19 @@ class StaticTests(TestCase):
|
|||
for filename in media_files:
|
||||
response = self.client.get('/views/%s/%s' % (self.prefix, filename))
|
||||
file_path = path.join(media_dir, filename)
|
||||
self.assertEqual(open(file_path).read(), response.content)
|
||||
self.assertEqual(len(response.content), int(response['Content-Length']))
|
||||
content = response.content
|
||||
self.assertEqual(open(file_path).read(), content)
|
||||
self.assertEqual(len(content), int(response['Content-Length']))
|
||||
self.assertEqual(mimetypes.guess_type(file_path)[1], response.get('Content-Encoding', None))
|
||||
|
||||
def test_chunked(self):
|
||||
"The static view should stream files in chunks to avoid large memory usage"
|
||||
response = self.client.get('/views/%s/%s' % (self.prefix, 'long-line.txt'))
|
||||
first_chunk = iter(response).next()
|
||||
self.assertEqual(len(first_chunk), STREAM_CHUNK_SIZE)
|
||||
second_chunk = response.next()
|
||||
self.assertEqual(len(second_chunk), 1451)
|
||||
|
||||
def test_unknown_mime_type(self):
|
||||
response = self.client.get('/views/%s/file.unknown' % self.prefix)
|
||||
self.assertEqual('application/octet-stream', response['Content-Type'])
|
||||
|
@ -71,9 +81,9 @@ class StaticTests(TestCase):
|
|||
response = self.client.get('/views/%s/%s' % (self.prefix, file_name),
|
||||
HTTP_IF_MODIFIED_SINCE=invalid_date)
|
||||
file = open(path.join(media_dir, file_name))
|
||||
self.assertEqual(file.read(), response.content)
|
||||
self.assertEqual(len(response.content),
|
||||
int(response['Content-Length']))
|
||||
content = response.content
|
||||
self.assertEqual(file.read(), content)
|
||||
self.assertEqual(len(content), int(response['Content-Length']))
|
||||
|
||||
def test_invalid_if_modified_since2(self):
|
||||
"""Handle even more bogus If-Modified-Since values gracefully
|
||||
|
@ -86,9 +96,9 @@ class StaticTests(TestCase):
|
|||
response = self.client.get('/views/%s/%s' % (self.prefix, file_name),
|
||||
HTTP_IF_MODIFIED_SINCE=invalid_date)
|
||||
file = open(path.join(media_dir, file_name))
|
||||
self.assertEqual(file.read(), response.content)
|
||||
self.assertEqual(len(response.content),
|
||||
int(response['Content-Length']))
|
||||
content = response.content
|
||||
self.assertEqual(file.read(), content)
|
||||
self.assertEqual(len(content), int(response['Content-Length']))
|
||||
|
||||
|
||||
class StaticHelperTest(StaticTests):
|
||||
|
|
Loading…
Reference in New Issue