From a850d479f00c63a3c0e22dbf95ec6bc4387f1824 Mon Sep 17 00:00:00 2001 From: Yongquan Date: Tue, 3 Nov 2026 06:56:11 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=89=E5=85=A8=EF=BC=9A=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E6=8E=A5=E5=8F=A3=E4=B8=89=E4=B8=AA=E7=9C=9F?= =?UTF-8?q?=E5=AE=9E=E6=BC=8F=E6=B4=9E=EF=BC=88=E5=8F=AF=E6=89=A7=E8=A1=8C?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E8=90=BD=E7=9B=98=20/=20=E9=99=90=E6=B5=81?= =?UTF-8?q?=E5=8F=AF=E7=BB=95=E8=BF=87=20/=20=E7=9C=9F=E4=B8=8A=E4=BC=A0?= =?UTF-8?q?=E6=97=A0=E9=99=90=E6=B5=81=EF=BC=89=20(#5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 用户提问 「上传视频和图片的接口,有没有限制?会不会所有人通过 POST 都能随意上传?」 ## 结论:防护大多到位,但存在三个真实漏洞 已核实有效的:CSRF、扩展名白名单、MIME 白名单、filesize 实测大小、 is_uploaded_file、类型开关、总量上限、uploads 目录 .htaccess 禁直连禁执行、 附件只走 media.php 代理。 但以下三个是真问题: ### 1. 🔴 可执行文件能写进 uploads/(最严重) MIME 只校验**前端上报的字符串**,从不检测文件真实内容。 实测:把 `` 改名成 .png、POST 里写 `mime=image/png`,一路通过所有检查写入磁盘。已确认真实文件内容: 00000000: 3c3f 7068 7020 7379 7374 656d 2824 5f47 $maxBits) { $bits = $maxBits; } + $bytes = intdiv($bits, 8); + $rem = $bits % 8; + if ($bytes > 0 && substr($netBin, 0, $bytes) !== substr($ipBin, 0, $bytes)) { continue; } + if ($rem === 0) { return true; } + $mask = 0xFF << (8 - $rem) & 0xFF; + if ((ord($netBin[$bytes]) & $mask) === (ord($ipBin[$bytes]) & $mask)) { return true; } + } + return false; } /** 访客 IP 的加盐哈希(不保存明文 IP) */ @@ -392,6 +469,7 @@ function fb_setting_keys() { 'footer.show_credit', 'footer.credit_url', 'installed_at', 'attachment.storage', + 'trusted_proxies', 'upload.image_on', 'upload.video_on', 'upload.image_max_per_post', 'upload.image_max_bytes', 'upload.video_max_per_post', 'upload.video_max_bytes', diff --git a/upload.php b/upload.php index 8309398..4fc1018 100644 --- a/upload.php +++ b/upload.php @@ -80,6 +80,17 @@ if ($putAt) { $ext = fb_upload_key_ext($key); if (!fb_upload_ext_ok($kind, $ext)) { fb_up_json(['ok' => false, 'error' => '不支持的文件格式。'], 400); } + /* 🔴 真上传(action=put)必须独立限流。 + * 原先只有「领凭证」那一步记 fb_rate_hit, + * 拿到一个新凭证后可以反复 POST action=put 覆盖写入,次数完全不受限。 + * 这里用同一个每小时额度,但独立计数(upload_put), + * 避免领凭证与落地互相挤占导致正常用户被误拦。 + */ + $putLimit = (int)fb_upload_cfg('upload_per_hour', 60); + if (!fb_rate_check('upload_put', $putLimit)) { + fb_up_json(['ok' => false, 'error' => '上传过于频繁,请稍后再试。'], 429); + } + if (empty($_FILES['file']) || !is_array($_FILES['file'])) { fb_up_json(['ok' => false, 'error' => '没有收到文件。'], 400); } @@ -102,6 +113,27 @@ if ($putAt) { if ($mime === '') { $mime = fb_upload_mime_of_ext($ext); } if (!fb_upload_mime_ok($kind, $mime)) { fb_up_json(['ok' => false, 'error' => '不支持的文件类型。'], 400); } + /* 🔴 关键:用文件真实内容再验一次 MIME,不信前端上报。 + * + * 事故:把 PHP 代码改名成 .png、POST 里写 mime=image/png, + * 就能一路通过所有检查写进 uploads/。 + * 实测确认文件内容就是 ``。 + * 虽然 .htaccess 禁了脚本执行,但那是唯一一道防线 —— + * 换 nginx / 改配置 / 目录迁移,任何一步疏忽就是 RCE。 + * + * 这里在**写盘之前**用 finfo 读文件头魔数, + * 真实类型必须也在白名单里,否则直接拒绝。 + */ + $realMime = fb_upload_detect_mime((string)$f['tmp_name']); + if ($realMime !== '' && !fb_upload_mime_ok($kind, $realMime)) { + fb_up_json([ + 'ok' => false, + 'error' => '文件内容与声明的类型不符(检测到 ' . $realMime . '),已拒绝。', + ], 415); + } + + fb_rate_hit('upload_put'); // 真正落地一次才计数 + // 二次确认总量上限(防止并发绕过) $pdo = fb_db(); $cap = (int)fb_upload_cfg('total_cap_bytes', 0);