bigywater.tech
Django · 133

媒体文件生产部署

用户上传的文件有一套自己的规矩:谁来提供、文件名会被怎样处理、大文件放在哪、有哪些上限。这篇在自己的测试应用上逐项验证,所有「恶意文件名」都只是对 FileField 自己的防护做测试。

/media/ 404
DEBUG=False 时 Django 不提供,static() 返回 0 条路由
docs/evil.txt
上传 '../../evil.txt' 保存后的文件名,仍在 MEDIA_ROOT 内
100
DATA_UPLOAD_MAX_NUMBER_FILES 默认值;101 个文件 → 400

谁来提供 /media/

实测 · 本机执行输出
  DEBUG = True → static(MEDIA_URL, document_root=…) 返回的路由数: 1
  GET /media/a.txt → 200
  DEBUG = False → static(MEDIA_URL, document_root=…) 返回的路由数: 0
  GET /media/a.txt → 404

文件名的处理

实测 · 本机执行输出
  上传 'a b.txt'                    → 保存为 'docs/a_b.txt'                                     在 MEDIA_ROOT 内: True  保存名长度 12
  上传 '../../evil.txt'             → 保存为 'docs/evil.txt'                                    在 MEDIA_ROOT 内: True  保存名长度 13
  上传 '/etc/passwd'                → 保存为 'docs/passwd'                                      在 MEDIA_ROOT 内: True  保存名长度 11
  上传 'rep.txt'                    → 保存为 'docs/rep.txt'                                     在 MEDIA_ROOT 内: True  保存名长度 12
  上传 'rep.txt'                    → 保存为 'docs/rep_UvQcZtV.txt'                             在 MEDIA_ROOT 内: True  保存名长度 20
  上传 '报告.txt'                     → 保存为 'docs/报告.txt'                                      在 MEDIA_ROOT 内: True  保存名长度 11
  上传 'xxxxxxxxxx…(304 字符)'        → 保存为 'docs/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'   在 MEDIA_ROOT 内: True  保存名长度 100
  上传 'page.html'                  → 保存为 'docs/page.html'                                   在 MEDIA_ROOT 内: True  保存名长度 14

这是 FileField.save() / 存储后端的处理。如果你自己拼路径(比如把用户给的文件名直接传给 open()),这些保护就不存在了。

内存与临时文件

实测 · 本机执行输出
  FILE_UPLOAD_MAX_MEMORY_SIZE = 2621440 字节
  上传 1 MiB → 请求里的文件对象类型 InMemoryUploadedFile,大小 1048576
  上传 3 MiB → 请求里的文件对象类型 TemporaryUploadedFile,大小 3145728

上传上限

实测 · 本机执行输出
  DATA_UPLOAD_MAX_MEMORY_SIZE 设成 1000 字节:
    上传 500000 字节的文件 → 200
    提交 2000 字节的普通字段 → 400
  DATA_UPLOAD_MAX_NUMBER_FILES = 100
    一次上传 100 个文件 → 200
    一次上传 101 个文件 → 400

文件本身的总大小没有被这两个设置限制,要限制上传大小,需要在代理(nginx 的 client_max_body_size)或视图 / 表单里做;本文没有测代理。

原样返回上传的文件

实测 · 本机执行输出
  GET /dl/page.html        Content-Type: text/html                  Content-Disposition: inline; filename="page.html"
  GET /dl/page.html?att=1  Content-Type: text/html                  Content-Disposition: attachment; filename="page.html"
  GET /dl/a.txt        Content-Type: text/plain                 Content-Disposition: inline; filename="a.txt"

用户上传的内容不应当用站点自己的域名、以 inline 方式提供;更稳妥的做法是用单独的域名或者强制下载。这是通用的安全建议,本文只验证了响应头的差别,没有在浏览器里执行。

分步演示

媒体文件上线
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 / Python 3.14.7 / Django 6.1.1(默认用 SQLite,其它数据库会在文中注明),用 django.test.Client 或真实的本机服务器进程实测;计时用 time.perf_counter,数字每次运行会有小幅抖动。SQLite;用 Client 上传 SimpleUploadedFile;所有文件写在项目自己的 media 目录
    • 没有验证:nginx 提供 /media/、S3 / 对象存储后端(django-storages)、图片处理(ImageField 的 Pillow 校验)、文件内容类型校验、病毒扫描、并发上传同名文件的竞态
    京ICP备2026067502号-1京公网安备11010502063487号