From 835c8c05da16114502522418104a81704001924d Mon Sep 17 00:00:00 2001 From: laoyang Date: Tue, 3 Nov 2026 08:13:21 +0800 Subject: [PATCH] =?UTF-8?q?=E8=BD=AF=E4=BB=B6=E5=9B=BE=E6=A0=87=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E4=B8=8A=E4=BC=A0=E5=9B=BE=E7=89=87=EF=BC=9A=E5=AD=98?= =?UTF-8?q?=20OSS/=E6=9C=AC=E6=9C=BA=EF=BC=8Cmedia.php=20=E4=BB=A3?= =?UTF-8?q?=E7=90=86=E8=AF=BB=E5=8F=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户要求图标可以自己上传一张真实图片,而不是只能填一个字符。 设计:字符和图片并存,图片优先 - software 加 icon_key(图片对象键),icon(字符)保留作兜底 - 有图片用图片,删掉图片自动退回字符,任何时刻都不会没有图标 - 前端 badge 与邮件头部同一套判断,页面和邮件永远一致 存储与读取 - 图标走独立键空间 icons/YYYY/MM/<16hex>. 不复用附件那套:附件必须挂在 thread 下才给读,图标不属于任何帖子 - 写:仅管理员(后台动作链)+ finfo 真实类型校验 + 尺寸下限 + 2MB 上限 - 读:media.php 对 icons/ 只校验键形状与文件存在(图标本就公开), 写入侧仍然严格,不会让本站变成能读整个桶的开放代理 - 优先用 media.php?k= 代理;OSS 下顺便借图片处理缩放,省下行 顺手修掉两个真实安全问题 1. fb_icon_key_ok 原来用 [a-z0-9]{2,5} 匹配扩展名,.php 正好能过 (3 个字母)—— 测试反向验证时发现,已改为图片扩展名白名单 2. fb_upload_key_ok 有同样的问题,附件键也能构造出 .php 虽然真上传还有 finfo 兜底,但键本身就不该长这样,一并收紧 3. fb_icon_delete 原来用 @ 吞掉错误,删除失败仍返回「已保存」, 用户会以为旧图清了、实际留了个可访问的孤儿图。改为返回 bool 并如实提示 验证:键校验 7 类非法输入全拒;图标写入 OSS 成功、media.php 读回 200 image/png、 PNG 魔数正确、缩放生效;越权读取(穿越/.php/未入库附件/不存在)全部按预期拒绝; 表单 enctype/文件框/accept/预览渲染齐全;仿真 PHP 伪图与 SVG 均被拒; 16x16 小图被拒。 --- assets/style.css | 22 +++++ lib/admin_taxonomy.php | 215 +++++++++++++++++++++++++++++++++++++++-- lib/attach.php | 72 ++++++++++++++ lib/bootstrap.php | 2 +- lib/db.php | 12 ++- lib/mailer.php | 24 ++++- lib/util.php | 14 +++ media.php | 37 ++++++- 8 files changed, 377 insertions(+), 21 deletions(-) diff --git a/assets/style.css b/assets/style.css index 9da4478..4ae8b52 100644 --- a/assets/style.css +++ b/assets/style.css @@ -149,6 +149,28 @@ main.wrap{padding-top:24px; padding-bottom:40px; min-height:60vh} display:inline-flex; align-items:center; justify-content:center; } .icon-preset:hover{border-color:var(--brand); background:var(--brand-soft)} + +/* ---------- 软件图标(上传图片) ---------- */ +/* 图标图片:外框与字符版完全一致,换图不会让布局跳动 */ +.sw-badge-img{ + display:inline-block; border-radius:7px; object-fit:cover; flex:none; + box-shadow:0 1px 2px rgba(15,25,45,.18); background:var(--card); +} +.chip.on .sw-badge-img{box-shadow:0 0 0 1px rgba(255,255,255,.5)} +/* 后台图标上传行 */ +.icon-up{display:flex; align-items:flex-start; gap:14px; flex-wrap:wrap; margin-top:6px} +.icon-prev{ + width:56px; height:56px; flex:none; border-radius:12px; overflow:hidden; + border:1px solid var(--line); background:var(--card); + display:flex; align-items:center; justify-content:center; +} +.icon-prev img{width:100%; height:100%; object-fit:cover; display:block} +.icon-prev-ph{ + width:100%; height:100%; display:flex; align-items:center; justify-content:center; + color:#fff; font-size:24px; font-weight:600; +} +.icon-up-main{flex:1; min-width:220px; display:flex; flex-direction:column} +.icon-up-main input[type=file]{font-size:13.5px; max-width:100%} /* 选中态的 chip 里,图标底衬白、字符取软件色,避免同色糊在一起 */ .chip.on .sw-badge-ic{background:#fff !important; color:var(--brand) !important; box-shadow:none} .chip.on .sw-badge-nm{color:#fff} diff --git a/lib/admin_taxonomy.php b/lib/admin_taxonomy.php index 24a93c8..8aed2c1 100644 --- a/lib/admin_taxonomy.php +++ b/lib/admin_taxonomy.php @@ -5,6 +5,123 @@ */ if (!defined('FB_ROOT')) { http_response_code(403); exit('forbidden'); } +/* ================================================================== + * 软件图标图片:上传 / 删除 + * ------------------------------------------------------------------ + * 安全边界(三条,缺一不可): + * ① 只有管理员能进(这个函数只从后台登录后的动作分支调用); + * ② 落盘前用 finfo 读真实文件头,必须是图片白名单里的类型 —— + * 与附件上传同一个教训:把 PHP 改名成 .png 就能写进目录, + * .htaccess 是唯一防线,换 nginx 就失守; + * ③ 键空间独立为 icons/,且形状受限(无穿越、无自由路径)。 + * + * 存储方式跟附件完全一致:本机 → uploads/;OSS → 服务器代理 PUT 到私有桶。 + * 对外一律 media.php?k=... 代理,不暴露真实位置。 + * ================================================================== */ + +/** 图标允许的扩展名(比附件白名单窄:图标不需要 gif 动图,也不要 svg) */ +function fb_icon_allowed_ext() { + // ⚠️ 刻意不收 svg:SVG 能内嵌