#33062 closed Bug (fixed)
File upload crash when a file extension contains null characters.
| Reported by: | Alex Vandiver | Owned by: | Hrushikesh Vaidya |
|---|---|---|---|
| Component: | File uploads/storage | Version: | 3.2 |
| Severity: | Normal | Keywords: | |
| Cc: | Florian Apolloner | Triage Stage: | Ready for checkin |
| Has patch: | yes | Needs documentation: | no |
| Needs tests: | no | Patch needs improvement: | no |
| Easy pickings: | yes | UI/UX: | no |
Description
A >2.5M file uploaded with a raw null byte anyplace after the . in its filename means that Django attempts to create a tempfile with that same "extension," which errors out with ValueError: embedded null byte.
It's almost certainly a violation of RFC to have a filename with a raw null byte in it, but it shouldn't result in a 500 when parsing the form.
Here's code to generate a bad request:
#!/usr/bin/env python3
import io
import requests
contents = io.StringIO("." * (1024 * 1024 * 3))
files = {"docfile": (b"bogus.txt!", contents, "text/plain")}
req = requests.Request("POST", "http://localhost:8000/", files=files, data={})
prepared = req.prepare()
body = prepared.body
assert isinstance(body, bytes)
prepared.body = body.replace(b"!", b"\x00")
requests.Session().send(prepared)
...which produces an error with the view:
from django import forms
from django.http import HttpResponseRedirect
from django.shortcuts import render
from django.views.decorators.csrf import csrf_exempt
class UploadFileForm(forms.Form):
docfile = forms.FileField()
@csrf_exempt
def index(request):
if request.method == 'POST':
form = UploadFileForm(request.POST, request.FILES)
if form.is_valid():
print(repr(request.FILES['docfile']))
return HttpResponseRedirect('/')
else:
print("Not valid!")
return HttpResponseRedirect('/')
else:
form = UploadFileForm()
return render(request, 'uploads/index.html', {'form': form})
I'm not sure what the goal is of preserving the "extension" of the uploaded file in the tempfile that is made; if that's important enough a behaviour to keep, some amount of escaping on the parsed-out extension may be necessary.
Change History (13)
comment:1 by , 4 years ago
comment:2 by , 4 years ago
| Cc: | added |
|---|---|
| Component: | Forms → File uploads/storage |
| Summary: | Null byte in uploaded filename extension is not cleaned up, results in exception → File upload crash when a file extension contains null characters. |
| Triage Stage: | Unreviewed → Accepted |
Thanks for the detailed report! I'd remove null characters in MultiPartParser.sanitize_file_name().
comment:3 by , 4 years ago
| Owner: | changed from to |
|---|---|
| Status: | new → assigned |
comment:5 by , 4 years ago
| Owner: | removed |
|---|---|
| Status: | assigned → new |
comment:6 by , 4 years ago
| Owner: | set to |
|---|---|
| Status: | new → assigned |
comment:7 by , 4 years ago
| Easy pickings: | set |
|---|---|
| Owner: | changed from to |
comment:9 by , 4 years ago
| Patch needs improvement: | unset |
|---|---|
| Triage Stage: | Accepted → Ready for checkin |
follow-up: 12 comment:11 by , 4 years ago
Thanks for the fix! Do you intend to backport this to the 3.2 LTS series?
comment:12 by , 4 years ago
Replying to Alex Vandiver:
Thanks for the fix! Do you intend to backport this to the 3.2 LTS series?
The issue has been present since the feature was introduced. Per our backporting policy this means it doesn't qualify for a backport to 3.2.x or 4.0.x anymore. See Django’s release process for more details.
Oh, and for completeness, this is the traceback:
Traceback (most recent call last): File "/usr/local/lib/python3.9/site-packages/django/core/handlers/exception.py", line 47, in inner response = get_response(request) File "/usr/local/lib/python3.9/site-packages/django/core/handlers/base.py", line 181, in _get_response response = wrapped_callback(request, *callback_args, **callback_kwargs) File "/usr/local/lib/python3.9/site-packages/django/views/decorators/csrf.py", line 54, in wrapped_view return view_func(*args, **kwargs) File "/Users/chmrr/src/test-django/upload_null_byte/uploads/views.py", line 14, in index form = UploadFileForm(request.POST, request.FILES) File "/usr/local/lib/python3.9/site-packages/django/core/handlers/wsgi.py", line 102, in _get_post self._load_post_and_files() File "/usr/local/lib/python3.9/site-packages/django/http/request.py", line 362, in _load_post_and_files self._post, self._files = self.parse_file_upload(self.META, data) File "/usr/local/lib/python3.9/site-packages/django/http/request.py", line 322, in parse_file_upload return parser.parse() File "/usr/local/lib/python3.9/site-packages/django/http/multipartparser.py", line 233, in parse handler.new_file( File "/usr/local/lib/python3.9/site-packages/django/core/files/uploadhandler.py", line 147, in new_file self.file = TemporaryUploadedFile(self.file_name, self.content_type, 0, self.charset, self.content_type_extra) File "/usr/local/lib/python3.9/site-packages/django/core/files/uploadedfile.py", line 64, in __init__ file = tempfile.NamedTemporaryFile(suffix='.upload' + ext, dir=settings.FILE_UPLOAD_TEMP_DIR) File "/usr/local/Cellar/python@3.9/3.9.5/Frameworks/Python.framework/Versions/3.9/lib/python3.9/tempfile.py", line 541, in NamedTemporaryFile (fd, name) = _mkstemp_inner(dir, prefix, suffix, flags, output_type) File "/usr/local/Cellar/python@3.9/3.9.5/Frameworks/Python.framework/Versions/3.9/lib/python3.9/tempfile.py", line 251, in _mkstemp_inner fd = _os.open(file, flags, 0o600) ValueError: embedded null byte