仅在服务器端接受FileField中的某种文件类型


74

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


1
要获取打开的对话框以将文件限制为客户端某些类型,请参见此问题
Flimm

Answers:


104

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

在您的应用程序中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])

另请参阅:如何限制具有FileFields的ModelForms的文件上载中的文件类型?

警告

用于保护代码执行环境免受恶意媒体文件的侵害

  1. 使用Exif正确验证媒体文件。
  2. 将媒体文件与应用程序代码执行环境分开
  3. 如果可能,请使用S3,GCS,Minio或类似的解决方案
  4. 在客户端上加载媒体文件时,请使用客户端本机方法(例如,如果您在浏览器中非安全地加载媒体文件,则可能会导致执行“精心制作的” JavaScript代码)

11
@dabad仅使用扩展名不利于文件验证,这些答案使安全漏洞;请看到这些答案的人也检查与不同格式和验证器有关的CVE,例如枕头/ PIL:O
Renjith Thankachan

1
@RenjithThankachan甚至有时可以使用任意代码来修改图像exif数据,但该技巧过去曾用于PHP应用程序。
Ammad Khalid

1
我们可以做的一件事是使用S3,Minio之类的解决方案将用户的媒体文件与应用程序代码执行环境@AmmadKhalid分开
Renjith Thankachan

83

Django版本1.11新增了一个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_extensionhttps://docs.djangoproject.com/en/dev/ref/validators/#validate-image-file-extension)用于验证图像扩展名(使用Pillow)。


3
即使allowed_extensions由于某种原因未添加文件,文件也已上传。使用Django Rest Framework。
Vineeth Sai

10

有一个Django片段可以做到这一点:

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()

2
这是基于扩展的过滤器,根本不可靠。我在考虑上传完成后分析文件。
maroxe 2010年

@maroxe您找到解决方案了吗?我只是确定是否文件是否为音频文件而遇到同样的问题。
marue


1
@ user126795-仍然不能真正确定它是正确的类型-似乎仅依赖于指定的内容类型-可能不可靠。
多米尼克·罗杰

2
您可以使用python-magic(libmagic的包装器)来获取mimetype,然后根据其接受/拒绝。
丹尼尔·奎因

8

第一。在应用程序内创建一个名为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

然后,使用此“ ContentTypeRestrictedFileField”代替“ FileField”。

例:

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中接受某种文件类型时必须要做的事情。


内容类型容易被骗
Trinh Hoang Nhu

3
在超级调用之前添加以下行:self.widget = ClearableFileInput(attrs = {'accept':','。join(self.content_types)})将使模态窗口中只能选择接受的内容类型。
laffuste

@laffuste支持您的评论,这似乎在Mac上不起作用(即,可以选择任何文件)。尚未在Windows下进行测试。
Erve1879

1
在chrome中有效,FF不会给出****。我发现按扩展名进行过滤总比内容类型好。后者几乎是由浏览器+操作系统任意设置的。
laffuste

8

一些人建议使用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,))

2
绝对是页面上最安全的答案!
符文卡加德

7

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

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

2

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


3
既然您确定这是最好的方法,为什么不发布一些代码向我们其他人演示它呢?
件事发生

2

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

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


1

另外,我将通过一些额外的行为来扩展本课程。

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”创建实例时,在默认情况下以文件结构弹出窗口中,我们将看到带有扩展名的文件。


1

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

https://docs.djangoproject.com/zh-CN/3.0/ref/validators/#fileextensionvalidator

警告

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.