XIUNO NEXT 重构记录贴(二十)附件存储加固
贰先生 2小时前

附件存储安全加固 Spec
Why
当前附件上传后文件名含 UID 前缀(uid_xn_rand(15).ext),可被猜解遍历;物理文件可通过 URL 直接访问,无需任何权限校验,存在越权下载风险。

What Changes
上传文件重命名为不可预测的随机字符串(32位),去除 UID 前缀
图片附件使用签名 URL 直接访问(防猜解,不走 PHP),非图片附件走 PHP 权限校验输出
upload/attach/ 目录禁止直接 HTTP 访问(.htaccess / Nginx 规则)
新增 /attach/{id}/{token} 路由用于图片签名访问,/attach/download/{id} 用于非图片下载
修改 attach_format() 生成新的安全 URL
仅新上传附件走新规则,历史附件保持兼容
Impact
Affected code: route/attach.php, model/attach.func.php, model/post.func.php, view/htm/thread.htm, upload/.htaccess
Affected configs: conf/conf.default.php(新增签名密钥配置)
不影响头像系统(头像独立存储,不在 bbs_attach 表中)
不新增数据库字段
ADDED Requirements
Requirement: 随机文件名生成
系统 SHALL 在附件上传时生成 32 位随机十六进制字符串作为文件名,保留原始扩展名,格式为 {32位随机hex}.{ext}。

Scenario: 新附件上传
WHEN 用户上传文件 报告.pdf
THEN 服务器存储为 upload/attach/202606/a3f8b2c1d4e5f6789012345678abcdef.pdf
AND bbs_attach.filename 记录为 202606/a3f8b2c1d4e5f6789012345678abcdef.pdf
AND bbs_attach.orgfilename 仍保留原始文件名 报告.pdf
Requirement: 图片签名 URL 访问
系统 SHALL 为图片类附件(isimage=1)生成带签名 token 的直接访问 URL,格式为 /attach/{aid}/{token}。

Scenario: 图片附件 URL 生成
WHEN attach_format() 处理 isimage=1 的附件
THEN 生成 URL 格式为 /attach/{aid}/{签名token}
AND token 由 md5(aid . filename . 密钥) 生成
AND token 长度为 32 位十六进制字符串
Scenario: 图片签名 URL 校验
WHEN 请求 /attach/{aid}/{token}
THEN 系统重新计算 token 并与请求中的 token 比对
AND token 匹配时,通过 X-Accel-Redirect(Nginx)或 readfile()(Apache)输出文件
AND token 不匹配时返回 403
AND 设置 Cache-Control: public, max-age=86400 允许浏览器缓存
Requirement: 非图片附件 PHP 权限校验输出
系统 SHALL 对非图片附件(isimage=0)通过 PHP 脚本进行权限校验后输出文件流。

Scenario: 非图片附件下载
WHEN 用户请求下载非图片附件
THEN 系统检查当前用户是否拥有下载权限(forum_access_user($fid, $gid, 'allowdown'))
AND 权限通过后通过 readfile() 输出文件,设置正确 Content-Type 和 Content-Disposition
AND 权限不足时返回错误提示
Requirement: upload/attach 目录禁止直接访问
系统 SHALL 在 upload/attach/ 目录下放置 .htaccess 规则,拒绝所有直接 HTTP 访问。

Scenario: 直接访问物理文件
WHEN 攻击者直接请求 upload/attach/202606/xxx.pdf
THEN 服务器返回 403 Forbidden
AND 仅通过 PHP 入口(签名 URL 或下载路由)可访问文件
Requirement: 签名密钥配置
系统 SHALL 在 conf/conf.default.php 中新增 attach_sign_key 配置项,用于生成和验证签名 token。

Scenario: 密钥初始化
WHEN 系统安装或升级
THEN 自动生成 32 位随机密钥存入配置
AND 密钥变更后所有旧签名 URL 失效
Requirement: 历史附件兼容
系统 SHALL 保持历史附件的现有访问方式不变,仅新上传附件使用新的安全机制。

Scenario: 历史附件访问
WHEN 访问历史附件(filename 中含 UID 前缀的旧格式)
THEN attach_format() 仍生成直接物理路径 URL(upload_url + attach/ + filename)
AND 历史附件不受 .htaccess 限制影响(仅 upload/attach/ 下新增子目录受限制)
Requirement: 可选防盗链
系统 SHALL 提供可配置的防盗链开关,根据 HTTP_REFERER 检查是否来自本站。

Scenario: 防盗链开启
WHEN 配置 attach_referer_check 为 TRUE
THEN 附件请求的 Referer 不在本站域名时返回 403
AND Referer 为空(直接访问)时允许通过
Requirement: 下载日志记录
系统 SHALL 记录非图片附件的下载行为,用于安全审计。

Scenario: 下载日志
WHEN 用户通过 PHP 入口下载非图片附件
THEN 记录附件 ID、用户 UID、IP、时间到日志文件 tmp/attach_download.log
MODIFIED Requirements
Requirement: attach_format URL 生成逻辑
原逻辑:$attach['url'] = $conf['upload_url'].'attach/'.$attach['filename']

新逻辑:

如果附件为新格式文件名(32位hex,不含 UID 前缀)且 isimage=1:生成签名 URL /attach/{aid}/{token}
如果附件为新格式文件名且 isimage=0:生成下载 URL /attach/download/{aid}
如果附件为旧格式文件名:保持原有直接物理路径 URL
Requirement: 附件上传文件名生成
原逻辑:$tmpname = $uid.'_'.xn_rand(15).'.'.$ext

新逻辑:$tmpname = bin2hex(random_bytes(16)).'.'.$ext(32位随机hex)
最后更新日期:2026.06.05
 
最新回复 (0)
全部楼主
返回