Repository: wagtail/wagtail

Issue:
Only ID required for document downloads
The document download only requires the ID for the downloads - since this isn't verifying that the passed in filename matches the requested file means that you can easily download any document of the website by just incrementing the ID value.

### Steps to Reproduce

1. Upload file to document
2. Copy link address for uploaded document, remove filename from URL
3. Visit URL, file gets downloaded

Even though one shouldn't necessarily store sensitive documents on a public facing website, this still means that somebody could easily gain access to any document hosted on the website.

### Technical details

* Django version: 1.10.6
* Wagtail version: 1.6



Discussion:

Resolving patch:
diff --git a/wagtail/wagtaildocs/views/serve.py b/wagtail/wagtaildocs/views/serve.py
--- a/wagtail/wagtaildocs/views/serve.py
+++ b/wagtail/wagtaildocs/views/serve.py
@@ -3,7 +3,7 @@
 from wsgiref.util import FileWrapper
 
 from django.conf import settings
-from django.http import BadHeaderError, StreamingHttpResponse
+from django.http import BadHeaderError, Http404, StreamingHttpResponse
 from django.shortcuts import get_object_or_404
 from unidecode import unidecode
 
@@ -16,6 +16,12 @@ def serve(request, document_id, document_filename):
     Document = get_document_model()
     doc = get_object_or_404(Document, id=document_id)
 
+    # We want to ensure that the document filename provided in the URL matches the one associated with the considered
+    # document_id. If not we can't be sure that the document the user wants to access is the one corresponding to the
+    # <document_id, document_filename> pair.
+    if doc.filename != document_filename:
+        raise Http404('This document does not match the given filename.')
+
     # Send document_served signal
     document_served.send(sender=Document, instance=doc, request=request)
 


Test patch:
diff --git a/wagtail/wagtaildocs/tests.py b/wagtail/wagtaildocs/tests.py
--- a/wagtail/wagtaildocs/tests.py
+++ b/wagtail/wagtaildocs/tests.py
@@ -1067,7 +1067,7 @@ def setUp(self):
         self.document.file.save('example.doc', ContentFile("A boring example document"))
 
     def get(self):
-        return self.client.get(reverse('wagtaildocs_serve', args=(self.document.id, 'example.doc')))
+        return self.client.get(reverse('wagtaildocs_serve', args=(self.document.id, self.document.filename)))
 
     def test_response_code(self):
         self.assertEqual(self.get().status_code, 200)
@@ -1104,14 +1104,8 @@ def test_with_nonexistent_document(self):
         self.assertEqual(response.status_code, 404)
 
     def test_with_incorrect_filename(self):
-        """
-        Wagtail should be forgiving with filenames at the end of the URL. These
-        filenames are to make the URL look nice, and to provide a fallback for
-        browsers that do not handle the 'Content-Disposition' header filename
-        component. They should not be validated.
-        """
         response = self.client.get(reverse('wagtaildocs_serve', args=(self.document.id, 'incorrectfilename')))
-        self.assertEqual(response.status_code, 200)
+        self.assertEqual(response.status_code, 404)
 
     def clear_sendfile_cache(self):
         from wagtail.utils.sendfile import _get_sendfile
@@ -1131,7 +1125,7 @@ def setUp(self):
         self.document.file.save('example.doc', ContentFile("A boring example document"))
 
     def get(self):
-        return self.client.get(reverse('wagtaildocs_serve', args=(self.document.id, 'example.doc')))
+        return self.client.get(reverse('wagtaildocs_serve', args=(self.document.id, self.document.filename)))
 
     def clear_sendfile_cache(self):
         from wagtail.utils.sendfile import _get_sendfile