【问题标题】:Only accept a certain file type in FileField, server-side仅接受 FileField 中的特定文件类型,服务器端
【发布时间】:2011-04-08 14:02:35
【问题描述】:

如何以优雅的方式在服务器端限制FileField 只接受某种类型的文件(视频、音频、pdf 等)?

【问题讨论】:

  • 要打开对话框以将文件限制为客户端的某些类型,see this question

标签: django file-upload


【解决方案1】:

有一个Django snippet 这样做:

import os

from django import forms

class ExtFileField(forms.FileField):
    """
    Same as forms.FileField, but you can specify a file extension whitelist.

    >>> from django.core.files.uploadedfile import SimpleUploadedFile
    >>>
    >>> t = ExtFileField(ext_whitelist=(".pdf", ".txt"))
    >>>
    >>> t.clean(SimpleUploadedFile('filename.pdf', 'Some File Content'))
    >>> t.clean(SimpleUploadedFile('filename.txt', 'Some File Content'))
    >>>
    >>> t.clean(SimpleUploadedFile('filename.exe', 'Some File Content'))
    Traceback (most recent call last):
    ...
    ValidationError: [u'Not allowed filetype!']
    """
    def __init__(self, *args, **kwargs):
        ext_whitelist = kwargs.pop("ext_whitelist")
        self.ext_whitelist = [i.lower() for i in ext_whitelist]

        super(ExtFileField, self).__init__(*args, **kwargs)

    def clean(self, *args, **kwargs):
        data = super(ExtFileField, self).clean(*args, **kwargs)
        filename = data.name
        ext = os.path.splitext(filename)[1]
        ext = ext.lower()
        if ext not in self.ext_whitelist:
            raise forms.ValidationError("Not allowed filetype!")

#-------------------------------------------------------------------------

if __name__ == "__main__":
    import doctest, datetime
    doctest.testmod()

【讨论】:

  • 这是一个基于扩展的过滤器,根本不可靠。我正在考虑在上传完成后分析文件。
  • @maroxe 你找到解决方案了吗?我在确定文件是否为音频文件时遇到了同样的问题。
  • 试试这个链接:nemesisdesign.net/blog/coding/…
  • @user126795 - 实际上仍然不能确定它是正确的类型 - 它似乎只是依赖于指定的内容类型 - 这可能不可靠。
  • 您可以使用 python-magic(libmagic 的包装器)来获取 mimetype,然后根据它接受/拒绝。
【解决方案2】:

首先。在应用程序中创建一个名为 formatChecker.py 的文件,其中您的模型具有您想要接受某种文件类型的 FileField。

这是你的 formatChecker.py:

from django.db.models import FileField
from django.forms import forms
from django.template.defaultfilters import filesizeformat
from django.utils.translation import ugettext_lazy as _

class ContentTypeRestrictedFileField(FileField):
    """
    Same as FileField, but you can specify:
        * content_types - list containing allowed content_types. Example: ['application/pdf', 'image/jpeg']
        * max_upload_size - a number indicating the maximum file size allowed for upload.
            2.5MB - 2621440
            5MB - 5242880
            10MB - 10485760
            20MB - 20971520
            50MB - 5242880
            100MB 104857600
            250MB - 214958080
            500MB - 429916160
"""
def __init__(self, *args, **kwargs):
    self.content_types = kwargs.pop("content_types")
    self.max_upload_size = kwargs.pop("max_upload_size")

    super(ContentTypeRestrictedFileField, self).__init__(*args, **kwargs)

def clean(self, *args, **kwargs):        
    data = super(ContentTypeRestrictedFileField, self).clean(*args, **kwargs)

    file = data.file
    try:
        content_type = file.content_type
        if content_type in self.content_types:
            if file._size > self.max_upload_size:
                raise forms.ValidationError(_('Please keep filesize under %s. Current filesize %s') % (filesizeformat(self.max_upload_size), filesizeformat(file._size)))
        else:
            raise forms.ValidationError(_('Filetype not supported.'))
    except AttributeError:
        pass        

    return data

第二。在你的 models.py 中,添加:

from formatChecker import ContentTypeRestrictedFileField

那么不要使用“FileField”,而是使用这个“ContentTypeRestrictedFileField”。

例子:

class Stuff(models.Model):
    title = models.CharField(max_length=245)
    handout = ContentTypeRestrictedFileField(upload_to='uploads/', content_types=['video/x-msvideo', 'application/pdf', 'video/mp4', 'audio/mpeg', ],max_upload_size=5242880,blank=True, null=True)

当您只想在 FileField 中接受某种文件类型时,您必须这样做。

【讨论】:

  • content-type 容易被骗
  • 在 super 调用之前添加这一行: self.widget = ClearableFileInput(attrs={ 'accept': ','.join(self.content_types) }) 将使仅接受的内容类型在模态窗口。
  • @laffuste 支持您的评论后,这似乎不适用于 Mac(即任何文件都是可选的)。不过还没有在 Windows 下测试过。
  • 在 chrome 中工作,FF 不提供 ****。我发现按扩展名过滤比按内容类型过滤要好。后者几乎是由你的浏览器+操作系统任意设置的。
【解决方案3】:

我认为您最适​​合使用 Dominic Rodger 在他的回答中指定的 ExtFileField 和 Daniel Quinn 提到的 python-magic 是最好的方法。如果有人足够聪明地更改扩展名,至少你会用标题抓住他们。

【讨论】:

  • 既然您已经确定这是最好的方法,为什么不发布一些代码来向我们其他人展示它!?
【解决方案4】:

一种非常简单的方法是使用自定义验证器。

在您应用的validators.py

def validate_file_extension(value):
    import os
    from django.core.exceptions import ValidationError
    ext = os.path.splitext(value.name)[1]  # [0] returns path+filename
    valid_extensions = ['.pdf', '.doc', '.docx', '.jpg', '.png', '.xlsx', '.xls']
    if not ext.lower() in valid_extensions:
        raise ValidationError('Unsupported file extension.')

然后在你的models.py:

from .validators import validate_file_extension

...并为您的表单字段使用验证器:

class Document(models.Model):
    file = models.FileField(upload_to="documents/%Y/%m/%d", validators=[validate_file_extension])

另请参阅:How to limit file types on file uploads for ModelForms with FileFields?

警告

用于保护您的代码执行环境免受恶意媒体文件的影响

  1. 使用 Exif libraries 正确验证媒体文件。
  2. 将媒体文件与应用程序代码分开 执行环境
  3. 如果可能,请使用 S3、GCS、Minio 或 任何类似的东西
  4. 在客户端加载媒体文件时,请使用客户端本地方法(例如,如果您在 浏览器,它可能会导致执行“精心设计的”JavaScript 代码)

【讨论】:

  • @dabad 仅使用扩展名不利于文件验证,这类答案会造成安全漏洞;请看到这些答案的人也检查与不同格式和验证器相关的 CVE,以枕头/PIL 为例:O
  • @RenjithThankachan 有时甚至可以使用任意代码修改图像 exif 数据,但这个技巧过去曾用于 PHP 应用程序。
  • 我们可以做的一件事是使用 S3、Minio 等解决方案将用户的媒体文件与应用程序代码执行环境分离@AmmadKhalid
【解决方案5】:

另外,我会用一些额外的行为来扩展这个类。

class ContentTypeRestrictedFileField(forms.FileField):
    ...
    widget = None
    ...
    def __init__(self, *args, **kwargs):
        ...
        self.widget = forms.ClearableFileInput(attrs={'accept':kwargs.pop('accept', None)})
        super(ContentTypeRestrictedFileField, self).__init__(*args, **kwargs)

当我们使用 param accept=".pdf,.txt" 创建实例时,在弹出窗口中默认文件结构我们将看到带有传递扩展名的文件。

【讨论】:

    【解决方案6】:

    您可以在设置中定义可接受的 mime 类型列表,然后定义一个验证器,该验证器使用 python-magic 来检测 mime-type 并在 mime-type 不被接受时引发 ValidationError。在文件表单字段中设置该验证器。

    唯一的问题是有时 mime 类型是 application/octet-stream,它可能对应不同的文件格式。你们中有人解决了这个问题吗?

    【讨论】:

      【解决方案7】:

      1.11 版本中的 Django 新增了一个用于模型字段的 FileExtensionValidator,文档在这里:https://docs.djangoproject.com/en/dev/ref/validators/#fileextensionvalidator

      如何验证文件扩展名的示例:

      from django.core.validators import FileExtensionValidator
      from django.db import models
      
      class MyModel(models.Model):
          pdf_file = models.FileField(upload_to='foo/',
                                      validators=[FileExtensionValidator(allowed_extensions=['pdf'])])
      

      请注意,此方法不安全。来自 Django 文档的引用:

      不要依赖文件扩展名的验证来确定文件的 类型。无论数据是什么,文件都可以重命名为具有任何扩展名 它们包含。

      还有新的validate_image_file_extension (https://docs.djangoproject.com/en/dev/ref/validators/#validate-image-file-extension) 用于验证图像扩展(使用 Pillow)。

      【讨论】:

      • 文件已上传,即使由于某种原因未添加到 allowed_extensions 中。使用 Django Rest 框架。
      【解决方案8】:

      您可以使用以下内容来限制表单中的文件类型

      file = forms.FileField(widget=forms.FileInput(attrs={'accept':'application/pdf'}))
      

      【讨论】:

      • 这是表单,但不验证模型
      【解决方案9】:

      一些人建议使用python-magic 来验证文件实际上是您期望接收的类型。这可以合并到接受的答案中建议的validator

      import os
      import magic
      from django.core.exceptions import ValidationError
      
      def validate_is_pdf(file):
          valid_mime_types = ['application/pdf']
          file_mime_type = magic.from_buffer(file.read(1024), mime=True)
          if file_mime_type not in valid_mime_types:
              raise ValidationError('Unsupported file type.')
          valid_file_extensions = ['.pdf']
          ext = os.path.splitext(file.name)[1]
          if ext.lower() not in valid_file_extensions:
              raise ValidationError('Unacceptable file extension.')
      

      此示例仅验证 pdf,但可以将任意数量的 mime 类型和文件扩展名添加到数组中。

      假设您将上述内容保存在 validators.py 中,您可以将其合并到您的模型中,如下所示:

      from myapp.validators import validate_is_pdf
      
      class PdfFile(models.Model):
          file = models.FileField(upload_to='pdfs/', validators=(validate_is_pdf,))
      

      【讨论】:

      • 绝对是页面上最安全的答案!
      • 如果它为特定的已知图像返回“application/octet-stream”,你需要这样做initial_pos = file.tell()file.seek(0)mime_type = magic.from_buffer(file.read(2048), mime=True)file.seek(initial_pos)
      • 我希望 Django 有一个内置的解决方案,可以在不安装第三方库的情况下确认已批准的 mime 类型和文件扩展名匹配。魔术库根据您的操作系统有不同的要求,这增加了部署过程中需要考虑的更多步骤。很好的答案,但不幸的是,这是必要的。
      【解决方案10】:

      在我检查了接受的答案后,我决定根据 Django 文档分享一个提示。已经有一个验证器用于验证文件扩展名。您无需重写自己的自定义函数来验证是否允许您的文件扩展名。

      https://docs.djangoproject.com/en/3.0/ref/validators/#fileextensionvalidator

      警告

      不要依赖文件扩展名的验证来确定文件的 类型。无论数据是什么,文件都可以重命名为具有任何扩展名 它们包含。

      【讨论】:

      【解决方案11】:

      @Thismatters 的回答稍作调整,因为我无法发表评论。根据python-magic的README:

      建议至少使用前 2048 个字节,因为少了会产生错误的标识

      因此将 1024 字节更改为 2048 以读取文件内容并从中获取 mime 类型基数可以给出最准确的结果,因此:

      def validate_extension(file):
          valid_mime_types = ["application/pdf", "image/jpeg", "image/png", "image/jpg"]
          file_mime_type = magic.from_buffer(file.read(2048), mime=True) #  Changed this to 1024 to 2048
      
          if file_mime_type not in valid_mime_types:
              raise ValidationError("Unsupported file type.")
      
          valid_file_extensions = [".pdf", ".jpeg", ".png", ".jpg"]
          ext = os.path.splitext(file.name)[1]
      
          if ext.lower() not in valid_file_extensions:
              raise ValidationError("Unacceptable file extension.")
      

      【讨论】:

        猜你喜欢
        • 2013-07-06
        • 1970-01-01
        • 2018-11-25
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2014-09-21
        • 1970-01-01
        相关资源
        最近更新 更多