You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Django序列化器中安全验证文件扩展名的最优实现方法

现有实现的问题

  • 仅校验文件扩展名完全不可靠:攻击者可以轻易把恶意脚本、可执行文件的后缀改成允许的图片格式绕过校验,上传后如果服务器存在解析漏洞的话直接会被执行,风险极高。
  • 校验逻辑放置位置不合理:你现在把校验写在create方法里,后续如果要加update逻辑也要复用上传功能的话,还要再写一遍校验,不符合DRY原则,也容易出现漏校验的情况。
  • 缺少其他必要校验:没有限制文件大小,很容易被上传超大文件打满服务器存储;没有过滤文件名,可能存在路径遍历漏洞风险。

更安全的实现方案

1. 优先校验文件魔数(MIME类型)而非扩展名

魔数是文件开头几个字节的固定标识,不会随扩展名修改而变化,用这个判断文件真实类型才可靠。可以搭配python-magic库校验真实文件类型,或用Pillow库打开图片校验合法性,顺便还能检查是不是损坏的图片。

2. 把校验逻辑放到序列化器字段验证层,复用性更高

直接给图片字段加validators,或者自定义图片校验器,所有用到上传功能的地方都能自动触发校验,避免漏写。

3. 附加其他安全校验

限制单文件大小、过滤特殊文件名、存储时重命名文件,不要直接用用户上传的原始文件名,避免路径遍历、特殊字符兼容问题。


代码示例

先安装依赖:
pip install python-magic pillow

优化后代码:

import os
import magic
from PIL import Image
from django.core.exceptions import ValidationError
from rest_framework import serializers

ALLOWED_IMAGE_EXTENSIONS = {"png", "jpg", "jpeg", "bmp", "gif"}
ALLOWED_MIME_TYPES = {"image/png", "image/jpeg", "image/bmp", "image/gif"}
MAX_IMAGE_SIZE = 5 * 1024 * 1024  # 单张图片最大限制5M

def validate_image(file):
    # 校验文件大小
    if file.size > MAX_IMAGE_SIZE:
        raise ValidationError(f"图片大小不能超过5M,当前文件大小:{file.size/1024/1024:.2f}M")
    
    # 校验扩展名
    ext = os.path.splitext(file.name)[1].lower().lstrip(".")
    if ext not in ALLOWED_IMAGE_EXTENSIONS:
        raise ValidationError(f"不支持的文件扩展名:{ext},仅支持{','.join(ALLOWED_IMAGE_EXTENSIONS)}")
    
    # 校验真实MIME类型
    mime_type = magic.from_buffer(file.read(1024), mime=True)
    file.seek(0)  # 读取完文件头后将指针复位,不影响后续文件处理
    if mime_type not in ALLOWED_MIME_TYPES:
        raise ValidationError(f"文件真实类型不合法:{mime_type},仅允许上传图片文件")
    
    # 可选增强校验:用Pillow打开确认图片完整合法,避免魔数伪造的恶意文件
    try:
        with Image.open(file) as img:
            img.verify()
        file.seek(0)
    except Exception as e:
        raise ValidationError("文件不是合法图片,或图片已损坏") from e

class ProductImageSerializer(serializers.ModelSerializer):
    # 假设模型中图片字段名为image,直接绑定校验器
    image = serializers.ImageField(validators=[validate_image])
    class Meta:
        model = ProductImage
        fields = ["id", "image"]

class ProductSerializer(serializers.ModelSerializer):
    images = ProductImageSerializer(many=True, read_only=True)
    # 新增上传用的写字段,自动触发校验
    uploaded_images = serializers.ListField(
        child=serializers.ImageField(validators=[validate_image]),
        write_only=True,
        required=False
    )

    class Meta:
        model = Product
        fields = ["id", "name", "images", "uploaded_images"]

    def create(self, validated_data):
        uploaded_images = validated_data.pop("uploaded_images", [])
        product = super().create(validated_data)
        for img in uploaded_images:
            ProductImage.objects.create(product=product, image=img)
        return product

额外安全建议

  • 存储图片的目录不要配置执行权限,静态资源单独用静态服务访问,不要让业务服务有执行目录内文件的权限。
  • 存储文件时自动重命名,比如用UUID作为文件名,不要直接使用用户上传的原始文件名。
  • 有条件的话将图片存储到对象存储服务,和业务服务器隔离,进一步降低安全风险。

内容的提问来源于stack exchange,提问作者some nooby questions

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.05 11:15:04