1. import base64
    
  2. import hashlib
    
  3. import os
    
  4. import shutil
    
  5. import sys
    
  6. import tempfile as sys_tempfile
    
  7. import unittest
    
  8. from io import BytesIO, StringIO
    
  9. from unittest import mock
    
  10. from urllib.parse import quote
    
  11. 
    
  12. from django.core.exceptions import SuspiciousFileOperation
    
  13. from django.core.files import temp as tempfile
    
  14. from django.core.files.uploadedfile import SimpleUploadedFile, UploadedFile
    
  15. from django.http.multipartparser import (
    
  16.     FILE,
    
  17.     MultiPartParser,
    
  18.     MultiPartParserError,
    
  19.     Parser,
    
  20.     parse_header,
    
  21. )
    
  22. from django.test import SimpleTestCase, TestCase, client, override_settings
    
  23. 
    
  24. from . import uploadhandler
    
  25. from .models import FileModel
    
  26. 
    
  27. UNICODE_FILENAME = "test-0123456789_中文_Orléans.jpg"
    
  28. MEDIA_ROOT = sys_tempfile.mkdtemp()
    
  29. UPLOAD_TO = os.path.join(MEDIA_ROOT, "test_upload")
    
  30. 
    
  31. CANDIDATE_TRAVERSAL_FILE_NAMES = [
    
  32.     "/tmp/hax0rd.txt",  # Absolute path, *nix-style.
    
  33.     "C:\\Windows\\hax0rd.txt",  # Absolute path, win-style.
    
  34.     "C:/Windows/hax0rd.txt",  # Absolute path, broken-style.
    
  35.     "\\tmp\\hax0rd.txt",  # Absolute path, broken in a different way.
    
  36.     "/tmp\\hax0rd.txt",  # Absolute path, broken by mixing.
    
  37.     "subdir/hax0rd.txt",  # Descendant path, *nix-style.
    
  38.     "subdir\\hax0rd.txt",  # Descendant path, win-style.
    
  39.     "sub/dir\\hax0rd.txt",  # Descendant path, mixed.
    
  40.     "../../hax0rd.txt",  # Relative path, *nix-style.
    
  41.     "..\\..\\hax0rd.txt",  # Relative path, win-style.
    
  42.     "../..\\hax0rd.txt",  # Relative path, mixed.
    
  43.     "../hax0rd.txt",  # HTML entities.
    
  44.     "../hax0rd.txt",  # HTML entities.
    
  45. ]
    
  46. 
    
  47. CANDIDATE_INVALID_FILE_NAMES = [
    
  48.     "/tmp/",  # Directory, *nix-style.
    
  49.     "c:\\tmp\\",  # Directory, win-style.
    
  50.     "/tmp/.",  # Directory dot, *nix-style.
    
  51.     "c:\\tmp\\.",  # Directory dot, *nix-style.
    
  52.     "/tmp/..",  # Parent directory, *nix-style.
    
  53.     "c:\\tmp\\..",  # Parent directory, win-style.
    
  54.     "",  # Empty filename.
    
  55. ]
    
  56. 
    
  57. 
    
  58. @override_settings(
    
  59.     MEDIA_ROOT=MEDIA_ROOT, ROOT_URLCONF="file_uploads.urls", MIDDLEWARE=[]
    
  60. )
    
  61. class FileUploadTests(TestCase):
    
  62.     @classmethod
    
  63.     def setUpClass(cls):
    
  64.         super().setUpClass()
    
  65.         os.makedirs(MEDIA_ROOT, exist_ok=True)
    
  66.         cls.addClassCleanup(shutil.rmtree, MEDIA_ROOT)
    
  67. 
    
  68.     def test_upload_name_is_validated(self):
    
  69.         candidates = [
    
  70.             "/tmp/",
    
  71.             "/tmp/..",
    
  72.             "/tmp/.",
    
  73.         ]
    
  74.         if sys.platform == "win32":
    
  75.             candidates.extend(
    
  76.                 [
    
  77.                     "c:\\tmp\\",
    
  78.                     "c:\\tmp\\..",
    
  79.                     "c:\\tmp\\.",
    
  80.                 ]
    
  81.             )
    
  82.         for file_name in candidates:
    
  83.             with self.subTest(file_name=file_name):
    
  84.                 self.assertRaises(SuspiciousFileOperation, UploadedFile, name=file_name)
    
  85. 
    
  86.     def test_simple_upload(self):
    
  87.         with open(__file__, "rb") as fp:
    
  88.             post_data = {
    
  89.                 "name": "Ringo",
    
  90.                 "file_field": fp,
    
  91.             }
    
  92.             response = self.client.post("/upload/", post_data)
    
  93.         self.assertEqual(response.status_code, 200)
    
  94. 
    
  95.     def test_large_upload(self):
    
  96.         file = tempfile.NamedTemporaryFile
    
  97.         with file(suffix=".file1") as file1, file(suffix=".file2") as file2:
    
  98.             file1.write(b"a" * (2**21))
    
  99.             file1.seek(0)
    
  100. 
    
  101.             file2.write(b"a" * (10 * 2**20))
    
  102.             file2.seek(0)
    
  103. 
    
  104.             post_data = {
    
  105.                 "name": "Ringo",
    
  106.                 "file_field1": file1,
    
  107.                 "file_field2": file2,
    
  108.             }
    
  109. 
    
  110.             for key in list(post_data):
    
  111.                 try:
    
  112.                     post_data[key + "_hash"] = hashlib.sha1(
    
  113.                         post_data[key].read()
    
  114.                     ).hexdigest()
    
  115.                     post_data[key].seek(0)
    
  116.                 except AttributeError:
    
  117.                     post_data[key + "_hash"] = hashlib.sha1(
    
  118.                         post_data[key].encode()
    
  119.                     ).hexdigest()
    
  120. 
    
  121.             response = self.client.post("/verify/", post_data)
    
  122. 
    
  123.             self.assertEqual(response.status_code, 200)
    
  124. 
    
  125.     def _test_base64_upload(self, content, encode=base64.b64encode):
    
  126.         payload = client.FakePayload(
    
  127.             "\r\n".join(
    
  128.                 [
    
  129.                     "--" + client.BOUNDARY,
    
  130.                     'Content-Disposition: form-data; name="file"; filename="test.txt"',
    
  131.                     "Content-Type: application/octet-stream",
    
  132.                     "Content-Transfer-Encoding: base64",
    
  133.                     "",
    
  134.                 ]
    
  135.             )
    
  136.         )
    
  137.         payload.write(b"\r\n" + encode(content.encode()) + b"\r\n")
    
  138.         payload.write("--" + client.BOUNDARY + "--\r\n")
    
  139.         r = {
    
  140.             "CONTENT_LENGTH": len(payload),
    
  141.             "CONTENT_TYPE": client.MULTIPART_CONTENT,
    
  142.             "PATH_INFO": "/echo_content/",
    
  143.             "REQUEST_METHOD": "POST",
    
  144.             "wsgi.input": payload,
    
  145.         }
    
  146.         response = self.client.request(**r)
    
  147.         self.assertEqual(response.json()["file"], content)
    
  148. 
    
  149.     def test_base64_upload(self):
    
  150.         self._test_base64_upload("This data will be transmitted base64-encoded.")
    
  151. 
    
  152.     def test_big_base64_upload(self):
    
  153.         self._test_base64_upload("Big data" * 68000)  # > 512Kb
    
  154. 
    
  155.     def test_big_base64_newlines_upload(self):
    
  156.         self._test_base64_upload("Big data" * 68000, encode=base64.encodebytes)
    
  157. 
    
  158.     def test_base64_invalid_upload(self):
    
  159.         payload = client.FakePayload(
    
  160.             "\r\n".join(
    
  161.                 [
    
  162.                     "--" + client.BOUNDARY,
    
  163.                     'Content-Disposition: form-data; name="file"; filename="test.txt"',
    
  164.                     "Content-Type: application/octet-stream",
    
  165.                     "Content-Transfer-Encoding: base64",
    
  166.                     "",
    
  167.                 ]
    
  168.             )
    
  169.         )
    
  170.         payload.write(b"\r\n!\r\n")
    
  171.         payload.write("--" + client.BOUNDARY + "--\r\n")
    
  172.         r = {
    
  173.             "CONTENT_LENGTH": len(payload),
    
  174.             "CONTENT_TYPE": client.MULTIPART_CONTENT,
    
  175.             "PATH_INFO": "/echo_content/",
    
  176.             "REQUEST_METHOD": "POST",
    
  177.             "wsgi.input": payload,
    
  178.         }
    
  179.         response = self.client.request(**r)
    
  180.         self.assertEqual(response.json()["file"], "")
    
  181. 
    
  182.     def test_unicode_file_name(self):
    
  183.         with sys_tempfile.TemporaryDirectory() as temp_dir:
    
  184.             # This file contains Chinese symbols and an accented char in the name.
    
  185.             with open(os.path.join(temp_dir, UNICODE_FILENAME), "w+b") as file1:
    
  186.                 file1.write(b"b" * (2**10))
    
  187.                 file1.seek(0)
    
  188.                 response = self.client.post("/unicode_name/", {"file_unicode": file1})
    
  189.             self.assertEqual(response.status_code, 200)
    
  190. 
    
  191.     def test_unicode_file_name_rfc2231(self):
    
  192.         """
    
  193.         Test receiving file upload when filename is encoded with RFC2231
    
  194.         (#22971).
    
  195.         """
    
  196.         payload = client.FakePayload()
    
  197.         payload.write(
    
  198.             "\r\n".join(
    
  199.                 [
    
  200.                     "--" + client.BOUNDARY,
    
  201.                     'Content-Disposition: form-data; name="file_unicode"; '
    
  202.                     "filename*=UTF-8''%s" % quote(UNICODE_FILENAME),
    
  203.                     "Content-Type: application/octet-stream",
    
  204.                     "",
    
  205.                     "You got pwnd.\r\n",
    
  206.                     "\r\n--" + client.BOUNDARY + "--\r\n",
    
  207.                 ]
    
  208.             )
    
  209.         )
    
  210. 
    
  211.         r = {
    
  212.             "CONTENT_LENGTH": len(payload),
    
  213.             "CONTENT_TYPE": client.MULTIPART_CONTENT,
    
  214.             "PATH_INFO": "/unicode_name/",
    
  215.             "REQUEST_METHOD": "POST",
    
  216.             "wsgi.input": payload,
    
  217.         }
    
  218.         response = self.client.request(**r)
    
  219.         self.assertEqual(response.status_code, 200)
    
  220. 
    
  221.     def test_unicode_name_rfc2231(self):
    
  222.         """
    
  223.         Test receiving file upload when filename is encoded with RFC2231
    
  224.         (#22971).
    
  225.         """
    
  226.         payload = client.FakePayload()
    
  227.         payload.write(
    
  228.             "\r\n".join(
    
  229.                 [
    
  230.                     "--" + client.BOUNDARY,
    
  231.                     "Content-Disposition: form-data; name*=UTF-8''file_unicode; "
    
  232.                     "filename*=UTF-8''%s" % quote(UNICODE_FILENAME),
    
  233.                     "Content-Type: application/octet-stream",
    
  234.                     "",
    
  235.                     "You got pwnd.\r\n",
    
  236.                     "\r\n--" + client.BOUNDARY + "--\r\n",
    
  237.                 ]
    
  238.             )
    
  239.         )
    
  240. 
    
  241.         r = {
    
  242.             "CONTENT_LENGTH": len(payload),
    
  243.             "CONTENT_TYPE": client.MULTIPART_CONTENT,
    
  244.             "PATH_INFO": "/unicode_name/",
    
  245.             "REQUEST_METHOD": "POST",
    
  246.             "wsgi.input": payload,
    
  247.         }
    
  248.         response = self.client.request(**r)
    
  249.         self.assertEqual(response.status_code, 200)
    
  250. 
    
  251.     def test_unicode_file_name_rfc2231_with_double_quotes(self):
    
  252.         payload = client.FakePayload()
    
  253.         payload.write(
    
  254.             "\r\n".join(
    
  255.                 [
    
  256.                     "--" + client.BOUNDARY,
    
  257.                     'Content-Disposition: form-data; name="file_unicode"; '
    
  258.                     "filename*=\"UTF-8''%s\"" % quote(UNICODE_FILENAME),
    
  259.                     "Content-Type: application/octet-stream",
    
  260.                     "",
    
  261.                     "You got pwnd.\r\n",
    
  262.                     "\r\n--" + client.BOUNDARY + "--\r\n",
    
  263.                 ]
    
  264.             )
    
  265.         )
    
  266.         r = {
    
  267.             "CONTENT_LENGTH": len(payload),
    
  268.             "CONTENT_TYPE": client.MULTIPART_CONTENT,
    
  269.             "PATH_INFO": "/unicode_name/",
    
  270.             "REQUEST_METHOD": "POST",
    
  271.             "wsgi.input": payload,
    
  272.         }
    
  273.         response = self.client.request(**r)
    
  274.         self.assertEqual(response.status_code, 200)
    
  275. 
    
  276.     def test_unicode_name_rfc2231_with_double_quotes(self):
    
  277.         payload = client.FakePayload()
    
  278.         payload.write(
    
  279.             "\r\n".join(
    
  280.                 [
    
  281.                     "--" + client.BOUNDARY,
    
  282.                     "Content-Disposition: form-data; name*=\"UTF-8''file_unicode\"; "
    
  283.                     "filename*=\"UTF-8''%s\"" % quote(UNICODE_FILENAME),
    
  284.                     "Content-Type: application/octet-stream",
    
  285.                     "",
    
  286.                     "You got pwnd.\r\n",
    
  287.                     "\r\n--" + client.BOUNDARY + "--\r\n",
    
  288.                 ]
    
  289.             )
    
  290.         )
    
  291.         r = {
    
  292.             "CONTENT_LENGTH": len(payload),
    
  293.             "CONTENT_TYPE": client.MULTIPART_CONTENT,
    
  294.             "PATH_INFO": "/unicode_name/",
    
  295.             "REQUEST_METHOD": "POST",
    
  296.             "wsgi.input": payload,
    
  297.         }
    
  298.         response = self.client.request(**r)
    
  299.         self.assertEqual(response.status_code, 200)
    
  300. 
    
  301.     def test_blank_filenames(self):
    
  302.         """
    
  303.         Receiving file upload when filename is blank (before and after
    
  304.         sanitization) should be okay.
    
  305.         """
    
  306.         filenames = [
    
  307.             "",
    
  308.             # Normalized by MultiPartParser.IE_sanitize().
    
  309.             "C:\\Windows\\",
    
  310.             # Normalized by os.path.basename().
    
  311.             "/",
    
  312.             "ends-with-slash/",
    
  313.         ]
    
  314.         payload = client.FakePayload()
    
  315.         for i, name in enumerate(filenames):
    
  316.             payload.write(
    
  317.                 "\r\n".join(
    
  318.                     [
    
  319.                         "--" + client.BOUNDARY,
    
  320.                         'Content-Disposition: form-data; name="file%s"; filename="%s"'
    
  321.                         % (i, name),
    
  322.                         "Content-Type: application/octet-stream",
    
  323.                         "",
    
  324.                         "You got pwnd.\r\n",
    
  325.                     ]
    
  326.                 )
    
  327.             )
    
  328.         payload.write("\r\n--" + client.BOUNDARY + "--\r\n")
    
  329. 
    
  330.         r = {
    
  331.             "CONTENT_LENGTH": len(payload),
    
  332.             "CONTENT_TYPE": client.MULTIPART_CONTENT,
    
  333.             "PATH_INFO": "/echo/",
    
  334.             "REQUEST_METHOD": "POST",
    
  335.             "wsgi.input": payload,
    
  336.         }
    
  337.         response = self.client.request(**r)
    
  338.         self.assertEqual(response.status_code, 200)
    
  339. 
    
  340.         # Empty filenames should be ignored
    
  341.         received = response.json()
    
  342.         for i, name in enumerate(filenames):
    
  343.             self.assertIsNone(received.get("file%s" % i))
    
  344. 
    
  345.     def test_non_printable_chars_in_file_names(self):
    
  346.         file_name = "non-\x00printable\x00\n_chars.txt\x00"
    
  347.         payload = client.FakePayload()
    
  348.         payload.write(
    
  349.             "\r\n".join(
    
  350.                 [
    
  351.                     "--" + client.BOUNDARY,
    
  352.                     f'Content-Disposition: form-data; name="file"; '
    
  353.                     f'filename="{file_name}"',
    
  354.                     "Content-Type: application/octet-stream",
    
  355.                     "",
    
  356.                     "You got pwnd.\r\n",
    
  357.                 ]
    
  358.             )
    
  359.         )
    
  360.         payload.write("\r\n--" + client.BOUNDARY + "--\r\n")
    
  361.         r = {
    
  362.             "CONTENT_LENGTH": len(payload),
    
  363.             "CONTENT_TYPE": client.MULTIPART_CONTENT,
    
  364.             "PATH_INFO": "/echo/",
    
  365.             "REQUEST_METHOD": "POST",
    
  366.             "wsgi.input": payload,
    
  367.         }
    
  368.         response = self.client.request(**r)
    
  369.         # Non-printable chars are sanitized.
    
  370.         received = response.json()
    
  371.         self.assertEqual(received["file"], "non-printable_chars.txt")
    
  372. 
    
  373.     def test_dangerous_file_names(self):
    
  374.         """Uploaded file names should be sanitized before ever reaching the view."""
    
  375.         # This test simulates possible directory traversal attacks by a
    
  376.         # malicious uploader We have to do some monkeybusiness here to construct
    
  377.         # a malicious payload with an invalid file name (containing os.sep or
    
  378.         # os.pardir). This similar to what an attacker would need to do when
    
  379.         # trying such an attack.
    
  380.         payload = client.FakePayload()
    
  381.         for i, name in enumerate(CANDIDATE_TRAVERSAL_FILE_NAMES):
    
  382.             payload.write(
    
  383.                 "\r\n".join(
    
  384.                     [
    
  385.                         "--" + client.BOUNDARY,
    
  386.                         'Content-Disposition: form-data; name="file%s"; filename="%s"'
    
  387.                         % (i, name),
    
  388.                         "Content-Type: application/octet-stream",
    
  389.                         "",
    
  390.                         "You got pwnd.\r\n",
    
  391.                     ]
    
  392.                 )
    
  393.             )
    
  394.         payload.write("\r\n--" + client.BOUNDARY + "--\r\n")
    
  395. 
    
  396.         r = {
    
  397.             "CONTENT_LENGTH": len(payload),
    
  398.             "CONTENT_TYPE": client.MULTIPART_CONTENT,
    
  399.             "PATH_INFO": "/echo/",
    
  400.             "REQUEST_METHOD": "POST",
    
  401.             "wsgi.input": payload,
    
  402.         }
    
  403.         response = self.client.request(**r)
    
  404.         # The filenames should have been sanitized by the time it got to the view.
    
  405.         received = response.json()
    
  406.         for i, name in enumerate(CANDIDATE_TRAVERSAL_FILE_NAMES):
    
  407.             got = received["file%s" % i]
    
  408.             self.assertEqual(got, "hax0rd.txt")
    
  409. 
    
  410.     def test_filename_overflow(self):
    
  411.         """File names over 256 characters (dangerous on some platforms) get fixed up."""
    
  412.         long_str = "f" * 300
    
  413.         cases = [
    
  414.             # field name, filename, expected
    
  415.             ("long_filename", "%s.txt" % long_str, "%s.txt" % long_str[:251]),
    
  416.             ("long_extension", "foo.%s" % long_str, ".%s" % long_str[:254]),
    
  417.             ("no_extension", long_str, long_str[:255]),
    
  418.             ("no_filename", ".%s" % long_str, ".%s" % long_str[:254]),
    
  419.             ("long_everything", "%s.%s" % (long_str, long_str), ".%s" % long_str[:254]),
    
  420.         ]
    
  421.         payload = client.FakePayload()
    
  422.         for name, filename, _ in cases:
    
  423.             payload.write(
    
  424.                 "\r\n".join(
    
  425.                     [
    
  426.                         "--" + client.BOUNDARY,
    
  427.                         'Content-Disposition: form-data; name="{}"; filename="{}"',
    
  428.                         "Content-Type: application/octet-stream",
    
  429.                         "",
    
  430.                         "Oops.",
    
  431.                         "",
    
  432.                     ]
    
  433.                 ).format(name, filename)
    
  434.             )
    
  435.         payload.write("\r\n--" + client.BOUNDARY + "--\r\n")
    
  436.         r = {
    
  437.             "CONTENT_LENGTH": len(payload),
    
  438.             "CONTENT_TYPE": client.MULTIPART_CONTENT,
    
  439.             "PATH_INFO": "/echo/",
    
  440.             "REQUEST_METHOD": "POST",
    
  441.             "wsgi.input": payload,
    
  442.         }
    
  443.         response = self.client.request(**r)
    
  444.         result = response.json()
    
  445.         for name, _, expected in cases:
    
  446.             got = result[name]
    
  447.             self.assertEqual(expected, got, "Mismatch for {}".format(name))
    
  448.             self.assertLess(
    
  449.                 len(got), 256, "Got a long file name (%s characters)." % len(got)
    
  450.             )
    
  451. 
    
  452.     def test_file_content(self):
    
  453.         file = tempfile.NamedTemporaryFile
    
  454.         with file(suffix=".ctype_extra") as no_content_type, file(
    
  455.             suffix=".ctype_extra"
    
  456.         ) as simple_file:
    
  457.             no_content_type.write(b"no content")
    
  458.             no_content_type.seek(0)
    
  459. 
    
  460.             simple_file.write(b"text content")
    
  461.             simple_file.seek(0)
    
  462.             simple_file.content_type = "text/plain"
    
  463. 
    
  464.             string_io = StringIO("string content")
    
  465.             bytes_io = BytesIO(b"binary content")
    
  466. 
    
  467.             response = self.client.post(
    
  468.                 "/echo_content/",
    
  469.                 {
    
  470.                     "no_content_type": no_content_type,
    
  471.                     "simple_file": simple_file,
    
  472.                     "string": string_io,
    
  473.                     "binary": bytes_io,
    
  474.                 },
    
  475.             )
    
  476.             received = response.json()
    
  477.             self.assertEqual(received["no_content_type"], "no content")
    
  478.             self.assertEqual(received["simple_file"], "text content")
    
  479.             self.assertEqual(received["string"], "string content")
    
  480.             self.assertEqual(received["binary"], "binary content")
    
  481. 
    
  482.     def test_content_type_extra(self):
    
  483.         """Uploaded files may have content type parameters available."""
    
  484.         file = tempfile.NamedTemporaryFile
    
  485.         with file(suffix=".ctype_extra") as no_content_type, file(
    
  486.             suffix=".ctype_extra"
    
  487.         ) as simple_file:
    
  488.             no_content_type.write(b"something")
    
  489.             no_content_type.seek(0)
    
  490. 
    
  491.             simple_file.write(b"something")
    
  492.             simple_file.seek(0)
    
  493.             simple_file.content_type = "text/plain; test-key=test_value"
    
  494. 
    
  495.             response = self.client.post(
    
  496.                 "/echo_content_type_extra/",
    
  497.                 {
    
  498.                     "no_content_type": no_content_type,
    
  499.                     "simple_file": simple_file,
    
  500.                 },
    
  501.             )
    
  502.             received = response.json()
    
  503.             self.assertEqual(received["no_content_type"], {})
    
  504.             self.assertEqual(received["simple_file"], {"test-key": "test_value"})
    
  505. 
    
  506.     def test_truncated_multipart_handled_gracefully(self):
    
  507.         """
    
  508.         If passed an incomplete multipart message, MultiPartParser does not
    
  509.         attempt to read beyond the end of the stream, and simply will handle
    
  510.         the part that can be parsed gracefully.
    
  511.         """
    
  512.         payload_str = "\r\n".join(
    
  513.             [
    
  514.                 "--" + client.BOUNDARY,
    
  515.                 'Content-Disposition: form-data; name="file"; filename="foo.txt"',
    
  516.                 "Content-Type: application/octet-stream",
    
  517.                 "",
    
  518.                 "file contents" "--" + client.BOUNDARY + "--",
    
  519.                 "",
    
  520.             ]
    
  521.         )
    
  522.         payload = client.FakePayload(payload_str[:-10])
    
  523.         r = {
    
  524.             "CONTENT_LENGTH": len(payload),
    
  525.             "CONTENT_TYPE": client.MULTIPART_CONTENT,
    
  526.             "PATH_INFO": "/echo/",
    
  527.             "REQUEST_METHOD": "POST",
    
  528.             "wsgi.input": payload,
    
  529.         }
    
  530.         self.assertEqual(self.client.request(**r).json(), {})
    
  531. 
    
  532.     def test_empty_multipart_handled_gracefully(self):
    
  533.         """
    
  534.         If passed an empty multipart message, MultiPartParser will return
    
  535.         an empty QueryDict.
    
  536.         """
    
  537.         r = {
    
  538.             "CONTENT_LENGTH": 0,
    
  539.             "CONTENT_TYPE": client.MULTIPART_CONTENT,
    
  540.             "PATH_INFO": "/echo/",
    
  541.             "REQUEST_METHOD": "POST",
    
  542.             "wsgi.input": client.FakePayload(b""),
    
  543.         }
    
  544.         self.assertEqual(self.client.request(**r).json(), {})
    
  545. 
    
  546.     def test_custom_upload_handler(self):
    
  547.         file = tempfile.NamedTemporaryFile
    
  548.         with file() as smallfile, file() as bigfile:
    
  549.             # A small file (under the 5M quota)
    
  550.             smallfile.write(b"a" * (2**21))
    
  551.             smallfile.seek(0)
    
  552. 
    
  553.             # A big file (over the quota)
    
  554.             bigfile.write(b"a" * (10 * 2**20))
    
  555.             bigfile.seek(0)
    
  556. 
    
  557.             # Small file posting should work.
    
  558.             self.assertIn("f", self.client.post("/quota/", {"f": smallfile}).json())
    
  559. 
    
  560.             # Large files don't go through.
    
  561.             self.assertNotIn("f", self.client.post("/quota/", {"f": bigfile}).json())
    
  562. 
    
  563.     def test_broken_custom_upload_handler(self):
    
  564.         with tempfile.NamedTemporaryFile() as file:
    
  565.             file.write(b"a" * (2**21))
    
  566.             file.seek(0)
    
  567. 
    
  568.             msg = (
    
  569.                 "You cannot alter upload handlers after the upload has been processed."
    
  570.             )
    
  571.             with self.assertRaisesMessage(AttributeError, msg):
    
  572.                 self.client.post("/quota/broken/", {"f": file})
    
  573. 
    
  574.     def test_stop_upload_temporary_file_handler(self):
    
  575.         with tempfile.NamedTemporaryFile() as temp_file:
    
  576.             temp_file.write(b"a")
    
  577.             temp_file.seek(0)
    
  578.             response = self.client.post("/temp_file/stop_upload/", {"file": temp_file})
    
  579.             temp_path = response.json()["temp_path"]
    
  580.             self.assertIs(os.path.exists(temp_path), False)
    
  581. 
    
  582.     def test_upload_interrupted_temporary_file_handler(self):
    
  583.         # Simulate an interrupted upload by omitting the closing boundary.
    
  584.         class MockedParser(Parser):
    
  585.             def __iter__(self):
    
  586.                 for item in super().__iter__():
    
  587.                     item_type, meta_data, field_stream = item
    
  588.                     yield item_type, meta_data, field_stream
    
  589.                     if item_type == FILE:
    
  590.                         return
    
  591. 
    
  592.         with tempfile.NamedTemporaryFile() as temp_file:
    
  593.             temp_file.write(b"a")
    
  594.             temp_file.seek(0)
    
  595.             with mock.patch(
    
  596.                 "django.http.multipartparser.Parser",
    
  597.                 MockedParser,
    
  598.             ):
    
  599.                 response = self.client.post(
    
  600.                     "/temp_file/upload_interrupted/",
    
  601.                     {"file": temp_file},
    
  602.                 )
    
  603.             temp_path = response.json()["temp_path"]
    
  604.             self.assertIs(os.path.exists(temp_path), False)
    
  605. 
    
  606.     def test_fileupload_getlist(self):
    
  607.         file = tempfile.NamedTemporaryFile
    
  608.         with file() as file1, file() as file2, file() as file2a:
    
  609.             file1.write(b"a" * (2**23))
    
  610.             file1.seek(0)
    
  611. 
    
  612.             file2.write(b"a" * (2 * 2**18))
    
  613.             file2.seek(0)
    
  614. 
    
  615.             file2a.write(b"a" * (5 * 2**20))
    
  616.             file2a.seek(0)
    
  617. 
    
  618.             response = self.client.post(
    
  619.                 "/getlist_count/",
    
  620.                 {
    
  621.                     "file1": file1,
    
  622.                     "field1": "test",
    
  623.                     "field2": "test3",
    
  624.                     "field3": "test5",
    
  625.                     "field4": "test6",
    
  626.                     "field5": "test7",
    
  627.                     "file2": (file2, file2a),
    
  628.                 },
    
  629.             )
    
  630.             got = response.json()
    
  631.             self.assertEqual(got.get("file1"), 1)
    
  632.             self.assertEqual(got.get("file2"), 2)
    
  633. 
    
  634.     def test_fileuploads_closed_at_request_end(self):
    
  635.         file = tempfile.NamedTemporaryFile
    
  636.         with file() as f1, file() as f2a, file() as f2b:
    
  637.             response = self.client.post(
    
  638.                 "/fd_closing/t/",
    
  639.                 {
    
  640.                     "file": f1,
    
  641.                     "file2": (f2a, f2b),
    
  642.                 },
    
  643.             )
    
  644. 
    
  645.         request = response.wsgi_request
    
  646.         # The files were parsed.
    
  647.         self.assertTrue(hasattr(request, "_files"))
    
  648. 
    
  649.         file = request._files["file"]
    
  650.         self.assertTrue(file.closed)
    
  651. 
    
  652.         files = request._files.getlist("file2")
    
  653.         self.assertTrue(files[0].closed)
    
  654.         self.assertTrue(files[1].closed)
    
  655. 
    
  656.     def test_no_parsing_triggered_by_fd_closing(self):
    
  657.         file = tempfile.NamedTemporaryFile
    
  658.         with file() as f1, file() as f2a, file() as f2b:
    
  659.             response = self.client.post(
    
  660.                 "/fd_closing/f/",
    
  661.                 {
    
  662.                     "file": f1,
    
  663.                     "file2": (f2a, f2b),
    
  664.                 },
    
  665.             )
    
  666. 
    
  667.         request = response.wsgi_request
    
  668.         # The fd closing logic doesn't trigger parsing of the stream
    
  669.         self.assertFalse(hasattr(request, "_files"))
    
  670. 
    
  671.     def test_file_error_blocking(self):
    
  672.         """
    
  673.         The server should not block when there are upload errors (bug #8622).
    
  674.         This can happen if something -- i.e. an exception handler -- tries to
    
  675.         access POST while handling an error in parsing POST. This shouldn't
    
  676.         cause an infinite loop!
    
  677.         """
    
  678. 
    
  679.         class POSTAccessingHandler(client.ClientHandler):
    
  680.             """A handler that'll access POST during an exception."""
    
  681. 
    
  682.             def handle_uncaught_exception(self, request, resolver, exc_info):
    
  683.                 ret = super().handle_uncaught_exception(request, resolver, exc_info)
    
  684.                 request.POST  # evaluate
    
  685.                 return ret
    
  686. 
    
  687.         # Maybe this is a little more complicated that it needs to be; but if
    
  688.         # the django.test.client.FakePayload.read() implementation changes then
    
  689.         # this test would fail.  So we need to know exactly what kind of error
    
  690.         # it raises when there is an attempt to read more than the available bytes:
    
  691.         try:
    
  692.             client.FakePayload(b"a").read(2)
    
  693.         except Exception as err:
    
  694.             reference_error = err
    
  695. 
    
  696.         # install the custom handler that tries to access request.POST
    
  697.         self.client.handler = POSTAccessingHandler()
    
  698. 
    
  699.         with open(__file__, "rb") as fp:
    
  700.             post_data = {
    
  701.                 "name": "Ringo",
    
  702.                 "file_field": fp,
    
  703.             }
    
  704.             try:
    
  705.                 self.client.post("/upload_errors/", post_data)
    
  706.             except reference_error.__class__ as err:
    
  707.                 self.assertNotEqual(
    
  708.                     str(err),
    
  709.                     str(reference_error),
    
  710.                     "Caught a repeated exception that'll cause an infinite loop in "
    
  711.                     "file uploads.",
    
  712.                 )
    
  713.             except Exception as err:
    
  714.                 # CustomUploadError is the error that should have been raised
    
  715.                 self.assertEqual(err.__class__, uploadhandler.CustomUploadError)
    
  716. 
    
  717.     def test_filename_case_preservation(self):
    
  718.         """
    
  719.         The storage backend shouldn't mess with the case of the filenames
    
  720.         uploaded.
    
  721.         """
    
  722.         # Synthesize the contents of a file upload with a mixed case filename
    
  723.         # so we don't have to carry such a file in the Django tests source code
    
  724.         # tree.
    
  725.         vars = {"boundary": "oUrBoUnDaRyStRiNg"}
    
  726.         post_data = [
    
  727.             "--%(boundary)s",
    
  728.             'Content-Disposition: form-data; name="file_field"; '
    
  729.             'filename="MiXeD_cAsE.txt"',
    
  730.             "Content-Type: application/octet-stream",
    
  731.             "",
    
  732.             "file contents\n",
    
  733.             "--%(boundary)s--\r\n",
    
  734.         ]
    
  735.         response = self.client.post(
    
  736.             "/filename_case/",
    
  737.             "\r\n".join(post_data) % vars,
    
  738.             "multipart/form-data; boundary=%(boundary)s" % vars,
    
  739.         )
    
  740.         self.assertEqual(response.status_code, 200)
    
  741.         id = int(response.content)
    
  742.         obj = FileModel.objects.get(pk=id)
    
  743.         # The name of the file uploaded and the file stored in the server-side
    
  744.         # shouldn't differ.
    
  745.         self.assertEqual(os.path.basename(obj.testfile.path), "MiXeD_cAsE.txt")
    
  746. 
    
  747.     def test_filename_traversal_upload(self):
    
  748.         os.makedirs(UPLOAD_TO, exist_ok=True)
    
  749.         tests = [
    
  750.             "../test.txt",
    
  751.             "../test.txt",
    
  752.         ]
    
  753.         for file_name in tests:
    
  754.             with self.subTest(file_name=file_name):
    
  755.                 payload = client.FakePayload()
    
  756.                 payload.write(
    
  757.                     "\r\n".join(
    
  758.                         [
    
  759.                             "--" + client.BOUNDARY,
    
  760.                             'Content-Disposition: form-data; name="my_file"; '
    
  761.                             'filename="%s";' % file_name,
    
  762.                             "Content-Type: text/plain",
    
  763.                             "",
    
  764.                             "file contents.\r\n",
    
  765.                             "\r\n--" + client.BOUNDARY + "--\r\n",
    
  766.                         ]
    
  767.                     ),
    
  768.                 )
    
  769.                 r = {
    
  770.                     "CONTENT_LENGTH": len(payload),
    
  771.                     "CONTENT_TYPE": client.MULTIPART_CONTENT,
    
  772.                     "PATH_INFO": "/upload_traversal/",
    
  773.                     "REQUEST_METHOD": "POST",
    
  774.                     "wsgi.input": payload,
    
  775.                 }
    
  776.                 response = self.client.request(**r)
    
  777.                 result = response.json()
    
  778.                 self.assertEqual(response.status_code, 200)
    
  779.                 self.assertEqual(result["file_name"], "test.txt")
    
  780.                 self.assertIs(
    
  781.                     os.path.exists(os.path.join(MEDIA_ROOT, "test.txt")),
    
  782.                     False,
    
  783.                 )
    
  784.                 self.assertIs(
    
  785.                     os.path.exists(os.path.join(UPLOAD_TO, "test.txt")),
    
  786.                     True,
    
  787.                 )
    
  788. 
    
  789. 
    
  790. @override_settings(MEDIA_ROOT=MEDIA_ROOT)
    
  791. class DirectoryCreationTests(SimpleTestCase):
    
  792.     """
    
  793.     Tests for error handling during directory creation
    
  794.     via _save_FIELD_file (ticket #6450)
    
  795.     """
    
  796. 
    
  797.     @classmethod
    
  798.     def setUpClass(cls):
    
  799.         super().setUpClass()
    
  800.         os.makedirs(MEDIA_ROOT, exist_ok=True)
    
  801.         cls.addClassCleanup(shutil.rmtree, MEDIA_ROOT)
    
  802. 
    
  803.     def setUp(self):
    
  804.         self.obj = FileModel()
    
  805. 
    
  806.     @unittest.skipIf(
    
  807.         sys.platform == "win32", "Python on Windows doesn't have working os.chmod()."
    
  808.     )
    
  809.     def test_readonly_root(self):
    
  810.         """Permission errors are not swallowed"""
    
  811.         os.chmod(MEDIA_ROOT, 0o500)
    
  812.         self.addCleanup(os.chmod, MEDIA_ROOT, 0o700)
    
  813.         with self.assertRaises(PermissionError):
    
  814.             self.obj.testfile.save(
    
  815.                 "foo.txt", SimpleUploadedFile("foo.txt", b"x"), save=False
    
  816.             )
    
  817. 
    
  818.     def test_not_a_directory(self):
    
  819.         # Create a file with the upload directory name
    
  820.         open(UPLOAD_TO, "wb").close()
    
  821.         self.addCleanup(os.remove, UPLOAD_TO)
    
  822.         msg = "%s exists and is not a directory." % UPLOAD_TO
    
  823.         with self.assertRaisesMessage(FileExistsError, msg):
    
  824.             with SimpleUploadedFile("foo.txt", b"x") as file:
    
  825.                 self.obj.testfile.save("foo.txt", file, save=False)
    
  826. 
    
  827. 
    
  828. class MultiParserTests(SimpleTestCase):
    
  829.     def test_empty_upload_handlers(self):
    
  830.         # We're not actually parsing here; just checking if the parser properly
    
  831.         # instantiates with empty upload handlers.
    
  832.         MultiPartParser(
    
  833.             {
    
  834.                 "CONTENT_TYPE": "multipart/form-data; boundary=_foo",
    
  835.                 "CONTENT_LENGTH": "1",
    
  836.             },
    
  837.             StringIO("x"),
    
  838.             [],
    
  839.             "utf-8",
    
  840.         )
    
  841. 
    
  842.     def test_invalid_content_type(self):
    
  843.         with self.assertRaisesMessage(
    
  844.             MultiPartParserError, "Invalid Content-Type: text/plain"
    
  845.         ):
    
  846.             MultiPartParser(
    
  847.                 {
    
  848.                     "CONTENT_TYPE": "text/plain",
    
  849.                     "CONTENT_LENGTH": "1",
    
  850.                 },
    
  851.                 StringIO("x"),
    
  852.                 [],
    
  853.                 "utf-8",
    
  854.             )
    
  855. 
    
  856.     def test_negative_content_length(self):
    
  857.         with self.assertRaisesMessage(
    
  858.             MultiPartParserError, "Invalid content length: -1"
    
  859.         ):
    
  860.             MultiPartParser(
    
  861.                 {
    
  862.                     "CONTENT_TYPE": "multipart/form-data; boundary=_foo",
    
  863.                     "CONTENT_LENGTH": -1,
    
  864.                 },
    
  865.                 StringIO("x"),
    
  866.                 [],
    
  867.                 "utf-8",
    
  868.             )
    
  869. 
    
  870.     def test_bad_type_content_length(self):
    
  871.         multipart_parser = MultiPartParser(
    
  872.             {
    
  873.                 "CONTENT_TYPE": "multipart/form-data; boundary=_foo",
    
  874.                 "CONTENT_LENGTH": "a",
    
  875.             },
    
  876.             StringIO("x"),
    
  877.             [],
    
  878.             "utf-8",
    
  879.         )
    
  880.         self.assertEqual(multipart_parser._content_length, 0)
    
  881. 
    
  882.     def test_sanitize_file_name(self):
    
  883.         parser = MultiPartParser(
    
  884.             {
    
  885.                 "CONTENT_TYPE": "multipart/form-data; boundary=_foo",
    
  886.                 "CONTENT_LENGTH": "1",
    
  887.             },
    
  888.             StringIO("x"),
    
  889.             [],
    
  890.             "utf-8",
    
  891.         )
    
  892.         for file_name in CANDIDATE_TRAVERSAL_FILE_NAMES:
    
  893.             with self.subTest(file_name=file_name):
    
  894.                 self.assertEqual(parser.sanitize_file_name(file_name), "hax0rd.txt")
    
  895. 
    
  896.     def test_sanitize_invalid_file_name(self):
    
  897.         parser = MultiPartParser(
    
  898.             {
    
  899.                 "CONTENT_TYPE": "multipart/form-data; boundary=_foo",
    
  900.                 "CONTENT_LENGTH": "1",
    
  901.             },
    
  902.             StringIO("x"),
    
  903.             [],
    
  904.             "utf-8",
    
  905.         )
    
  906.         for file_name in CANDIDATE_INVALID_FILE_NAMES:
    
  907.             with self.subTest(file_name=file_name):
    
  908.                 self.assertIsNone(parser.sanitize_file_name(file_name))
    
  909. 
    
  910.     def test_rfc2231_parsing(self):
    
  911.         test_data = (
    
  912.             (
    
  913.                 b"Content-Type: application/x-stuff; "
    
  914.                 b"title*=us-ascii'en-us'This%20is%20%2A%2A%2Afun%2A%2A%2A",
    
  915.                 "This is ***fun***",
    
  916.             ),
    
  917.             (
    
  918.                 b"Content-Type: application/x-stuff; title*=UTF-8''foo-%c3%a4.html",
    
  919.                 "foo-ä.html",
    
  920.             ),
    
  921.             (
    
  922.                 b"Content-Type: application/x-stuff; title*=iso-8859-1''foo-%E4.html",
    
  923.                 "foo-ä.html",
    
  924.             ),
    
  925.         )
    
  926.         for raw_line, expected_title in test_data:
    
  927.             parsed = parse_header(raw_line)
    
  928.             self.assertEqual(parsed[1]["title"], expected_title)
    
  929. 
    
  930.     def test_rfc2231_wrong_title(self):
    
  931.         """
    
  932.         Test wrongly formatted RFC 2231 headers (missing double single quotes).
    
  933.         Parsing should not crash (#24209).
    
  934.         """
    
  935.         test_data = (
    
  936.             (
    
  937.                 b"Content-Type: application/x-stuff; "
    
  938.                 b"title*='This%20is%20%2A%2A%2Afun%2A%2A%2A",
    
  939.                 b"'This%20is%20%2A%2A%2Afun%2A%2A%2A",
    
  940.             ),
    
  941.             (b"Content-Type: application/x-stuff; title*='foo.html", b"'foo.html"),
    
  942.             (b"Content-Type: application/x-stuff; title*=bar.html", b"bar.html"),
    
  943.         )
    
  944.         for raw_line, expected_title in test_data:
    
  945.             parsed = parse_header(raw_line)
    
  946.             self.assertEqual(parsed[1]["title"], expected_title)
    
  947. 
    
  948.     def test_parse_header_with_double_quotes_and_semicolon(self):
    
  949.         self.assertEqual(
    
  950.             parse_header(b'form-data; name="files"; filename="fo\\"o;bar"'),
    
  951.             ("form-data", {"name": b"files", "filename": b'fo"o;bar'}),
    
  952.         )