上传接口安全:XFF 可绕限流、真上传无限流、size 只信前端 #5

Open
opened 2026-11-02 22:51:24 +00:00 by laoyang · 0 comments
Owner

用户提问:上传接口有没有安全限制?会不会所有人都能随意 POST 上传

上传视频和图片的接口,有没有限制?就是说有没有安全限制,如果没有,岂不是所有人,通过 post,都可以随意的上传视频和图片了吗?

结论:上传本体防护是够的,但限流可被一行请求头绕过。

匿名上传在防护上是设计允许的(这是个不需要注册的反馈系统,访客当然要能传图),
但「允许多少、多大」必须有护栏。护栏本身有漏洞。

已核实的防护(有效)

检查项 实现 实测
必须是 POST REQUEST_METHOD 405
存储关闭则拒绝 fb_store_mode() === 'off' → 403 ✅ 403
本机目录不可写则拒绝 fb_local_writable() ✅
CSRF fb_csrf_ok() + hash_equals ✅ 无 token 返回「会话已过期」
文件类型(扩展名白名单) fb_upload_ext_ok() ✅ .php/.phtml/.svg 全 400
文件类型(MIME 白名单) fb_upload_mime_ok() ✅
真实文件大小 filesize($f['tmp_name']) 实测,不信前端报的数 ✅
必须是真上传临时文件 is_uploaded_file() ✅ 防伪造 tmp_name
单文件上限 image_max_bytes / video_max_bytes ✅
全站总量上限 total_cap_bytes(领凭证 + 落地各查一次) ✅
附件类型开关 upload.image_on / video_on ✅
uploads 不可直连 .htaccess v2:Require all denied + 禁脚本执行 ✅ 线上 /uploads/ → 403
附件只走 media.php 代理 前端 URL 全是 media.php?k= ✅ 真实路径不暴露
每 IP 每小时次数 fb_rate_check('upload') ⚠️ 可绕过,见下

🔴 漏洞 1:伪造 X-Forwarded-For 可无限绕过限流

lib/util.php:26 fb_client_ip():

$keys = ['HTTP_CF_CONNECTING_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_REAL_IP', 'REMOTE_ADDR'];
foreach ($keys as $k) {
    if (!empty($_SERVER[$k])) {
        $v = explode(',', (string)$_SERVER[$k]);
        $v = trim($v[0]);
        if (filter_var($v, FILTER_VALIDATE_IP)) { return $v; }
    }
}

无条件信任 X-Forwarded-For 等请求头 —— 而这些头是客户端可以随便写的。

实测(upload_per_hour = 2,连发 6 次、每次换一个 XFF):

200,200,200,200,200,200   ← 上限 2,却全部放行

影响面不止上传:fb_ip_hash() 用同一个函数,
所以「发帖频率限制」「验证码频率」「任何基于 IP 的限流」全部失效。
攻击者随便改个头就能无限发帖、无限上传、无限刷验证码。

正确做法:只有在「明确知道自己在反向代理后面」时才信任这些头,
并且只信任固定层数(例如只取最后一跳的 XFF)。
默认应直接用 REMOTE_ADDR。

🔴 漏洞 2:领凭证接口只信前端报的 size

「领凭证」这一步(POST upload.php 不带 action=put):

$size = (int)(isset($_POST['size']) ? $_POST['size'] : 0);
...
if ($maxBytes > 0 && $size > $maxBytes) { 拒绝 }

size 是前端自己报的。报一个小数字就能拿到凭证,
随后「真上传」那一步才用 filesize() 实测拦下来。

结果:拦住的是数据落地,不是请求本身。
攻击者仍可用超大文件反复触发请求、耗带宽和临时磁盘(upload_max_filesize 以内)。

实测:报 size=1060 但实际传 1MB 的图 → 领凭证返回 200。

修法:领凭证阶段不承担大小校验职责(本来也做不到),
但真上传阶段必须独立限流,且 post_max_size / upload_max_filesize
要在层配置上卡死(这是服务器层的事,代码层只能兜底)。

🔴 漏洞 3:真上传路径没有限流

fb_rate_hit('upload') 只在领凭证成功时记一次。
action=put(真正写入磁盘/OSS)那一段完全没有 fb_rate_check。

意味着:拿到一个凭证后,可以反复 POST action=put 同一 key 覆盖写入。
虽然单文件大小仍受限,但没有次数约束。

要做的事

  1. fb_client_ip() 默认只信 REMOTE_ADDR;新增配置项
    trusted_proxies(逗号分隔 CIDR),只有请求来自这些地址时才解析 XFF,
    且只取最右一跳(不可伪造的那一跳)。
  2. 真上传路径加独立限流(fb_rate_check('upload_put', ...) + fb_rate_hit)。
  3. 领凭证阶段把 size 当作「提示值」,不用于放行判断;
    改为在真上传阶段严格用 filesize()(已如此),并把
    upload_max_filesize / post_max_size 的检查项加入后台「存储体检」提示。

验收

  • 改 XFF 六次,超过上限后必须返回 429
  • 真上传连续超限必须返回 429
  • 正常用户(真实单 IP)上传体验不受影响
  • 现有 33/33 安装测试、22/22 附件测试、73/73 邮件测试不回归

为什么单独建单

与 #1(多软件归属)、#2(邮件通知)、#3(表单预填)、#4(附件 0 值)都无关:这是上传接口的访问控制与限流安全,涉及 IP 识别、真上传限流、服务器层配置,可独立验收

## 用户提问:上传接口有没有安全限制?会不会所有人都能随意 POST 上传 > 上传视频和图片的接口,有没有限制?就是说有没有安全限制,如果没有,岂不是所有人,通过 post,都可以随意的上传视频和图片了吗? ## 结论:**上传本体防护是够的,但限流可被一行请求头绕过。** 匿名上传在防护上**是设计允许的**(这是个不需要注册的反馈系统,访客当然要能传图), 但「允许多少、多大」必须有护栏。护栏本身有漏洞。 ## 已核实的防护(有效) | 检查项 | 实现 | 实测 | |---|---|---| | 必须是 POST | `REQUEST_METHOD` | 405 | | 存储关闭则拒绝 | `fb_store_mode() === 'off'` → 403 | ✅ 403 | | 本机目录不可写则拒绝 | `fb_local_writable()` | ✅ | | **CSRF** | `fb_csrf_ok()` + `hash_equals` | ✅ 无 token 返回「会话已过期」 | | 文件类型(扩展名白名单) | `fb_upload_ext_ok()` | ✅ `.php`/`.phtml`/`.svg` 全 400 | | 文件类型(MIME 白名单) | `fb_upload_mime_ok()` | ✅ | | **真实文件大小** | `filesize($f['tmp_name'])` 实测,不信前端报的数 | ✅ | | 必须是真上传临时文件 | `is_uploaded_file()` | ✅ 防伪造 tmp_name | | 单文件上限 | `image_max_bytes` / `video_max_bytes` | ✅ | | 全站总量上限 | `total_cap_bytes`(领凭证 + 落地各查一次) | ✅ | | 附件类型开关 | `upload.image_on` / `video_on` | ✅ | | **uploads 不可直连** | `.htaccess` v2:`Require all denied` + 禁脚本执行 | ✅ 线上 /uploads/ → 403 | | 附件只走 media.php 代理 | 前端 URL 全是 `media.php?k=` | ✅ 真实路径不暴露 | | 每 IP 每小时次数 | `fb_rate_check('upload')` | ⚠️ **可绕过,见下** | ## 🔴 漏洞 1:伪造 X-Forwarded-For 可无限绕过限流 `lib/util.php:26 fb_client_ip()`: ```php $keys = ['HTTP_CF_CONNECTING_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_REAL_IP', 'REMOTE_ADDR']; foreach ($keys as $k) { if (!empty($_SERVER[$k])) { $v = explode(',', (string)$_SERVER[$k]); $v = trim($v[0]); if (filter_var($v, FILTER_VALIDATE_IP)) { return $v; } } } ``` **无条件信任** `X-Forwarded-For` 等请求头 —— 而这些头是客户端可以随便写的。 实测(`upload_per_hour = 2`,连发 6 次、每次换一个 XFF): ``` 200,200,200,200,200,200 ← 上限 2,却全部放行 ``` **影响面不止上传**:`fb_ip_hash()` 用同一个函数, 所以「发帖频率限制」「验证码频率」「任何基于 IP 的限流」全部失效。 攻击者随便改个头就能无限发帖、无限上传、无限刷验证码。 正确做法:只有在「明确知道自己在反向代理后面」时才信任这些头, 并且只信任固定层数(例如只取最后一跳的 XFF)。 默认应直接用 `REMOTE_ADDR`。 ## 🔴 漏洞 2:领凭证接口只信前端报的 size 「领凭证」这一步(`POST upload.php` 不带 `action=put`): ```php $size = (int)(isset($_POST['size']) ? $_POST['size'] : 0); ... if ($maxBytes > 0 && $size > $maxBytes) { 拒绝 } ``` `size` 是**前端自己报的**。报一个小数字就能拿到凭证, 随后「真上传」那一步才用 `filesize()` 实测拦下来。 **结果:拦住的是数据落地,不是请求本身。** 攻击者仍可用超大文件反复触发请求、耗带宽和临时磁盘(`upload_max_filesize` 以内)。 实测:报 `size=1060` 但实际传 1MB 的图 → 领凭证返回 200。 修法:领凭证阶段不承担大小校验职责(本来也做不到), 但**真上传阶段必须独立限流**,且 `post_max_size` / `upload_max_filesize` 要在层配置上卡死(这是服务器层的事,代码层只能兜底)。 ## 🔴 漏洞 3:真上传路径没有限流 `fb_rate_hit('upload')` 只在**领凭证成功时**记一次。 `action=put`(真正写入磁盘/OSS)那一段**完全没有 `fb_rate_check`**。 意味着:拿到一个凭证后,可以反复 POST `action=put` 同一 key 覆盖写入。 虽然单文件大小仍受限,但没有次数约束。 ## 要做的事 1. `fb_client_ip()` 默认只信 `REMOTE_ADDR`;新增配置项 `trusted_proxies`(逗号分隔 CIDR),只有请求来自这些地址时才解析 XFF, 且只取**最右一跳**(不可伪造的那一跳)。 2. 真上传路径加独立限流(`fb_rate_check('upload_put', ...)` + `fb_rate_hit`)。 3. 领凭证阶段把 `size` 当作「提示值」,不用于放行判断; 改为在真上传阶段严格用 `filesize()`(已如此),并把 `upload_max_filesize` / `post_max_size` 的检查项加入后台「存储体检」提示。 ## 验收 - 改 XFF 六次,超过上限后必须返回 429 - 真上传连续超限必须返回 429 - 正常用户(真实单 IP)上传体验不受影响 - 现有 33/33 安装测试、22/22 附件测试、73/73 邮件测试不回归 --- ### 为什么单独建单 与 #1(多软件归属)、#2(邮件通知)、#3(表单预填)、#4(附件 0 值)都无关:这是上传接口的访问控制与限流安全,涉及 IP 识别、真上传限流、服务器层配置,可独立验收
Sign in to join this conversation.
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: laoyang/FeedLite#5