Insecure file upload - Python

Insecure file upload - Python

Need

Secure file upload and validation

Context

  • Usage of Python 3 for developing Python applications
  • Usage of Django for building web applications in Python

Description

Non compliant code

        from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt
from django.core.files.storage import default_storage

@csrf_exempt
def upload_file(request):
    if request.method == 'POST':
        file = request.FILES['file']
        file_name = default_storage.save(file.name, file)
        file_url = default_storage.url(file_name)

        return HttpResponse(file_url)
        
        

This is a simple Django view function upload_file that handles file uploads. The function is decorated with @csrf_exempt which means it does not check for CSRF tokens. This is a security vulnerability as it leaves the application open to Cross-Site Request Forgery attacks.

The function first checks if the request method is 'POST'. If it is, it retrieves the file from the request using request.FILES['file']. The file is then saved to the default storage and the URL of the saved file is returned in the HTTP response.

The vulnerability here is that there is no validation of the uploaded file. The system allows the upload of files with two extensions or with unsafe ones, such as .html, .php or .exe. Furthermore, the system does not validate whether or not the file is free from malicious code. This means that a malicious user could upload a file containing harmful code, which could then be executed on the server or on the client's machine.

This is a serious security issue as it could lead to the compromise of the server or the client's machine. It is therefore crucial to validate all uploaded files to ensure they are safe before saving them to the server.

Steps

  • Validate the file extension. Only allow safe extensions and disallow double extensions.
  • Implement server-side validation to check if the file contains any malicious code.
  • Limit the file size to prevent large file uploads which can lead to Denial of Service (DoS) attacks.
  • Store the uploaded files in a separate directory that doesn't have execute permissions, to prevent any potential code in the file from being executed.
  • Rename the file upon upload to ensure that the file name can't be used to trigger any vulnerabilities.
  • Use a library or a service that provides secure file upload capabilities.

Compliant code

        import os
from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt
from django.core.files.storage import default_storage
from django.core.exceptions import ValidationError

ALLOWED_EXTENSIONS = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'pdf', 'doc', 'docx']
MAX_FILE_SIZE = 1024 * 1024  # 1MB

def validate_file(file):
    ext = file.name.split('.')[-1]
    # Check file extension
    if ext not in ALLOWED_EXTENSIONS or file.name.count('.') > 1:
        raise ValidationError("Unsupported file extension.")
    # Check file size
    if file.size > MAX_FILE_SIZE:
        raise ValidationError("File size exceeds the limit.")
    # TODO: Add server-side validation to check if the file contains any malicious code

@csrf_exempt
def upload_file(request):
    if request.method == 'POST':
        file = request.FILES['file']
        try:
            validate_file(file)
            # Rename the file upon upload
            file_name = default_storage.save('uploaded_files/' + os.urandom(16).hex() + '.' + file.name.split('.')[-1], file)
            file_url = default_storage.url(file_name)
            return HttpResponse(file_url)
        except ValidationError as e:
            return HttpResponse(e)
        
        

The updated code includes a function validate_file that validates the uploaded file. It checks the file extension and size. Only files with allowed extensions and size less than the limit can be uploaded. The allowed extensions and maximum file size are defined as constants at the beginning of the code.

The file extension is extracted from the file name and checked against the list of allowed extensions. Files with double extensions are also disallowed. The file size is checked to be less than the maximum allowed size.

The validate_file function is called in the upload_file view before saving the file. If the file fails validation, a ValidationError is raised and returned in the HTTP response.

The uploaded file is saved in a separate directory 'uploaded_files/' and the file is renamed upon upload using a random string generated by os.urandom(16).hex(). This ensures that the original file name can't be used to trigger any vulnerabilities.

Note: The code includes a placeholder for adding server-side validation to check if the file contains any malicious code. This can be implemented using a library or service that provides secure file upload capabilities.

References