1
0
mirror of https://github.com/django/django.git synced 2025-07-18 16:49:13 +00:00

[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:
Tim Graham 2014-12-09 15:32:03 -05:00
parent 4c241f1b71
commit d020da6646
4 changed files with 39 additions and 10 deletions

View File

@ -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.http import http_date, parse_http_date
from django.utils.translation import ugettext as _, ugettext_noop from django.utils.translation import ugettext as _, ugettext_noop
STREAM_CHUNK_SIZE = 4096
def serve(request, path, document_root=None, show_indexes=False): def serve(request, path, document_root=None, show_indexes=False):
""" """
Serve static files below a given point in the directory structure. 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'), if not was_modified_since(request.META.get('HTTP_IF_MODIFIED_SINCE'),
statobj.st_mtime, statobj.st_size): statobj.st_mtime, statobj.st_size):
return HttpResponseNotModified(mimetype=mimetype) return HttpResponseNotModified(mimetype=mimetype)
with open(fullpath, 'rb') as f: f = open(fullpath, 'rb')
response = HttpResponse(f.read(), mimetype=mimetype) response = HttpResponse(iter(lambda: f.read(STREAM_CHUNK_SIZE), ''), mimetype=mimetype)
response["Last-Modified"] = http_date(statobj.st_mtime) response["Last-Modified"] = http_date(statobj.st_mtime)
if stat.S_ISREG(statobj.st_mode): if stat.S_ISREG(statobj.st_mode):
response["Content-Length"] = statobj.st_size response["Content-Length"] = statobj.st_size

View File

@ -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 this URL into the ``Location`` response header and browsers seem to ignore
JavaScript there. 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 Bugfixes
======== ========

File diff suppressed because one or more lines are too long

View File

@ -7,6 +7,7 @@ from django.conf import settings
from django.conf.urls.static import static from django.conf.urls.static import static
from django.test import TestCase from django.test import TestCase
from django.http import HttpResponseNotModified from django.http import HttpResponseNotModified
from django.views.static import STREAM_CHUNK_SIZE
from .. import urls from .. import urls
from ..urls import media_dir from ..urls import media_dir
@ -29,10 +30,19 @@ class StaticTests(TestCase):
for filename in media_files: for filename in media_files:
response = self.client.get('/views/%s/%s' % (self.prefix, filename)) response = self.client.get('/views/%s/%s' % (self.prefix, filename))
file_path = path.join(media_dir, filename) file_path = path.join(media_dir, filename)
self.assertEqual(open(file_path).read(), response.content) content = response.content
self.assertEqual(len(response.content), int(response['Content-Length'])) 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)) 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): def test_unknown_mime_type(self):
response = self.client.get('/views/%s/file.unknown' % self.prefix) response = self.client.get('/views/%s/file.unknown' % self.prefix)
self.assertEqual('application/octet-stream', response['Content-Type']) 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), response = self.client.get('/views/%s/%s' % (self.prefix, file_name),
HTTP_IF_MODIFIED_SINCE=invalid_date) HTTP_IF_MODIFIED_SINCE=invalid_date)
file = open(path.join(media_dir, file_name)) file = open(path.join(media_dir, file_name))
self.assertEqual(file.read(), response.content) content = response.content
self.assertEqual(len(response.content), self.assertEqual(file.read(), content)
int(response['Content-Length'])) self.assertEqual(len(content), int(response['Content-Length']))
def test_invalid_if_modified_since2(self): def test_invalid_if_modified_since2(self):
"""Handle even more bogus If-Modified-Since values gracefully """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), response = self.client.get('/views/%s/%s' % (self.prefix, file_name),
HTTP_IF_MODIFIED_SINCE=invalid_date) HTTP_IF_MODIFIED_SINCE=invalid_date)
file = open(path.join(media_dir, file_name)) file = open(path.join(media_dir, file_name))
self.assertEqual(file.read(), response.content) content = response.content
self.assertEqual(len(response.content), self.assertEqual(file.read(), content)
int(response['Content-Length'])) self.assertEqual(len(content), int(response['Content-Length']))
class StaticHelperTest(StaticTests): class StaticHelperTest(StaticTests):