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
相关产品推荐
相关产品推荐

