在Python开发Web应用或后端服务时,文件上传是常见功能,如果仅验证文件后缀名,攻击者可以修改文件后缀伪造合法文件,因此同时验证后缀名和MIME Type是更安全的做法。

一、文件后缀名验证的实现
后缀名验证是最基础的文件类型限制方式,逻辑比较简单,只需要获取上传文件的名称,提取后缀后和允许的后缀列表做比对即可。
基础后缀名验证示例
以下是使用Flask框架实现后缀名验证的示例代码:
from flask import Flask, request
from werkzeug.utils import secure_filename
app = Flask(__name__)
# 允许上传的文件后缀列表
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'}
def allowed_file(filename):
# 检查文件名是否包含点,且后缀在允许列表中
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
@app.route('/upload', methods=['POST'])
def upload_file():
if 'file' not in request.files:
return '未上传文件', 400
file = request.files['file']
if file.filename == '':
return '未选择文件', 400
if file and allowed_file(file.filename):
filename = secure_filename(file.filename)
# 保存文件的逻辑
return '文件上传成功', 200
else:
return '不允许的文件类型', 400
if __name__ == '__main__':
app.run(debug=True)
后缀名验证的局限性
这种方式存在明显缺陷,攻击者可以将恶意脚本的后缀修改为jpg等合法后缀,就能绕过验证,因此后缀名验证只能作为第一层过滤,不能作为唯一的安全校验手段。
二、MIME Type验证的实现
MIME Type是标识文件类型的标准,上传文件的MIME Type可以通过请求头中的Content-Type获取,也可以通过读取文件内容判断真实的MIME Type。
基于请求头Content-Type的验证
这种方式获取MIME Type的速度快,但同样可以被伪造,示例代码如下:
from flask import Flask, request
app = Flask(__name__)
# 允许的MIME Type列表
ALLOWED_MIME_TYPES = {'image/png', 'image/jpeg', 'image/gif'}
@app.route('/upload_mime', methods=['POST'])
def upload_file_mime():
if 'file' not in request.files:
return '未上传文件', 400
file = request.files['file']
# 获取请求的Content-Type
file_mime = file.content_type
if file_mime in ALLOWED_MIME_TYPES:
# 保存文件的逻辑
return '文件上传成功', 200
else:
return '不允许的文件类型', 400
if __name__ == '__main__':
app.run(debug=True)
基于文件内容判断真实MIME Type
要获取文件真实的MIME Type,需要读取文件的内容进行判断,Python的python-magic库可以实现这个功能,首先需要安装依赖:
pip install python-magic
实现示例代码如下:
import magic
from flask import Flask, request
app = Flask(__name__)
# 允许的MIME Type列表
ALLOWED_MIME_TYPES = {'image/png', 'image/jpeg', 'image/gif'}
def get_real_mime(file_stream):
# 读取文件前2048字节判断MIME Type,足够识别大部分文件类型
file_head = file_stream.read(2048)
# 将文件指针移回开头,避免后续保存文件时内容缺失
file_stream.seek(0)
mime = magic.from_buffer(file_head, mime=True)
return mime
@app.route('/upload_real_mime', methods=['POST'])
def upload_file_real_mime():
if 'file' not in request.files:
return '未上传文件', 400
file = request.files['file']
real_mime = get_real_mime(file.stream)
if real_mime in ALLOWED_MIME_TYPES:
# 保存文件的逻辑
return '文件上传成功', 200
else:
return '不允许的文件类型', 400
if __name__ == '__main__':
app.run(debug=True)
三、结合两种验证的完整方案
最安全的做法是同时验证文件后缀名和真实MIME Type,两者都通过才允许上传,示例代码如下:
import magic
from flask import Flask, request
from werkzeug.utils import secure_filename
app = Flask(__name__)
# 允许的后缀列表
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'}
# 允许的MIME Type列表
ALLOWED_MIME_TYPES = {'image/png', 'image/jpeg', 'image/gif'}
def allowed_file(filename):
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
def get_real_mime(file_stream):
file_head = file_stream.read(2048)
file_stream.seek(0)
return magic.from_buffer(file_head, mime=True)
@app.route('/upload_safe', methods=['POST'])
def upload_file_safe():
if 'file' not in request.files:
return '未上传文件', 400
file = request.files['file']
if file.filename == '':
return '未选择文件', 400
# 第一层:后缀名验证
if not allowed_file(file.filename):
return '不允许的文件后缀', 400
# 第二层:真实MIME Type验证
real_mime = get_real_mime(file.stream)
if real_mime not in ALLOWED_MIME_TYPES:
return '不允许的文件类型', 400
# 两层验证都通过,保存文件
filename = secure_filename(file.filename)
return '文件上传成功', 200
if __name__ == '__main__':
app.run(debug=True)
四、注意事项
- 不要只依赖单一验证方式,后缀名和MIME Type验证要结合使用
- 读取文件内容判断MIME Type时,不需要读取整个文件,读取前几KB即可,避免大文件占用过多内存
- 上传的文件不要保存在Web可访问目录,或者如果保存在可访问目录,需要关闭文件的执行权限,避免恶意脚本被直接执行
- 对上传的文件名使用
secure_filename处理,避免路径 traversal 攻击
通过以上方法,就可以在Python中实现安全可靠的 upload 文件类型限制,有效降低恶意文件上传带来的安全风险。