此文章为XIUNOX版本重构审计时发现问题,XIUNOX版本已优化修复此问题。分享出来方便后续想基于xiuno bbs4.0.4版本制作维护版本或插件模板等需求的开发者和站长参考。
现象
Xiuno BBS 4.0.4 多处代码在未初始化变量或未判空数组键的情况下直接读取。在 PHP 7 时代这些是 E_NOTICE,默认不显示;PHP 8.0 起未定义变量升级为 Warning,PHP 8.1 起对 null/false 进行数组下标访问升级为 TypeError(致命错误)。开启 error_reporting(E_ALL) 时(xiunophp.php 第 18 行在 DEBUG 模式下默认开启),日志被大量污染,部分场景直接致命中断。
源码证据
证据 1:route/user.php 第 22-23 行(用户主页)—— empty 检查被注释掉
// 文件 xiunobbs_4.0.4/route/user.php 第 19-23 行 $_uid = param(1, 0); empty($_uid) AND $_uid = $uid; $_user = user_read($_uid); // empty($_user) AND message(-1, lang('user_not_exists')); <-- 第 21 行:检查被注释! $header['title'] = $_user['username']; <-- 第 22 行 $header['mobile_title'] = $_user['username']; <-- 第 23 行
当访问 /user-0.htm 且未登录($uid = 0)时,$_uid = 0,user_read(0) 在 model/user.func.php 第 67 行 if(empty($uid)) return array(); 返回空数组,第 22 行 $_user['username'] 触发 Warning: Undefined array key "username";若 user_read 因 DB 错误返回 FALSE,PHP 8.1 起直接 TypeError: Cannot access offset on bool。
证据 2:route/user.php 第 37-43 行 —— 检查后未提前退出
// 第 35-43 行 $_user = user_read($_uid); empty($_user) AND message(-1, lang('user_not_exists')); // message() 默认不 die $header['title'] = $_user['username']; // 仍会执行,触发 Warning $header['mobile_title'] = $_user['username']; $page = param(3, 1); $pagesize = 20; $totalnum = $_user['threads']; // 同上,未判空
message() 函数在 DEBUG < 2 时通过 xn_message() 输出 JSON 并 exit;但若在测试/插件 hook 中被覆盖、或 message 第 3 参数为非 exit 模式,后续 $_user['threads'] 仍会执行。
证据 3:route/thread.php 第 30 行 —— 帖子详情页读取空 forum
// 第 20-30 行 $fid = param(2, 0); $forum = $fid ? forum_read($fid) : array(); // fid=0 时返回 array() ... $header['mobile_title'] = $fid ? $forum['name'] : ''; // 第 30 行
虽然有三元运算符保护,但 $fid 为 truthy 时(如 fid=999 且版块已删除),forum_read() 返回 array(),第 30 行 $forum['name'] 触发 Warning: Undefined array key "name"。
证据 4:route/post.php 第 115 行 —— 帖子更新读取可能不存在的字段
// 第 103-119 行 $pid = param(2); $post = post_read($pid); empty($post) AND message(-1, lang('post_not_exists:')); $tid = $post['tid']; // 第 107 行:依赖 post 表必有 tid 字段 $thread = thread_read($tid); empty($thread) AND message(-1, lang('thread_not_exists:')); $fid = $thread['fid']; $forum = forum_read($fid); empty($forum) AND message(-1, lang('forum_not_exists:')); $isfirst = $post['isfirst']; // 第 115 行:依赖 post 表必有 isfirst 字段
post_read() 返回的数组字段由 bbs_post 表结构决定,若插件扩展时未保证字段一定存在,第 107、115 行触发 Warning: Undefined array key。
证据 5:index.inc.php 第 22 行 —— group 兜底访问
// 第 22 行 $group = isset($grouplist[$gid]) ? $grouplist[$gid] : $grouplist[0];
$grouplist 来自 group_list_cache()。若缓存为空或被插件清空,$grouplist[0] 触发 Warning: Undefined array key 0,并赋值 NULL,后续 $group['name'] 等访问连锁报错。
风险等级与结论
兼容障碍(运行时 Warning,部分场景致命)。PHP 8.0+ 升级了未定义变量/数组键的报错级别,PHP 8.1+ 对 false/null 数组访问直接 TypeError。
危害:
- DEBUG 模式(
error_reporting(E_ALL))下,error_log 被海量Warning: Undefined array key淹没,影响真实问题排查。 - PHP 8.1+ 下,当
user_read/forum_read/post_read因 DB 异常返回 FALSE 时,后续$_user['username']直接 TypeError 致命错误,白屏。 - 业务逻辑:用户主页、帖子详情页在脏数据场景下访问残留字段,产生未定义索引告警,影响用户体验。
- 第三方插件依赖
$_user必有username字段,空数组场景下插件渲染失败。
修复建议(最少改动版):
route/user.php第 21 行恢复empty($_user) AND message(-1, lang('user_not_exists'));(去掉注释)。- 所有
xxx_read()返回空时,紧接empty($xxx) AND message(...)后追加exit;或确保message()一定exit。 - 对访问数组键前加
?或isset保护,例:
$header['title'] = $_user['username'] ?? ''; $totalnum = $_user['threads'] ?? 0; $isfirst = $post['isfirst'] ?? 0;
index.inc.php第 22 行:$group = $grouplist[$gid] ?? ($grouplist[0] ?? array());。- 项目级修复:在
model/*.func.php的*_format()函数顶部统一$arr += array('field1'=>default1, 'field2'=>default2);兜底默认值。