此文章为XIUNOX版本重构审计时发现问题,XIUNOX版本已优化修复此问题。分享出来方便后续想基于xiuno bbs4.0.4版本制作维护版本或插件模板等需求的开发者和站长参考。
现象
Xiuno BBS 4.0.4 业务代码(route/*.php、model/*.func.php)中几乎不存在 try/catch 与 throw,错误通过全局变量 $errno/$errstr + xn_error() 函数传递,调用方仅能通过 === FALSE 返回值感知错误,错误信息靠 global $errstr 读取。这种"全局错误变量"模式在嵌套调用时会被覆盖、无法定位错误源头。同时 trigger_error 散落各处(统计 15 处),部分致命错误直接 exit 或 message(-1, ...) 终止流程,无统一异常处理器收集上下文。
源码证据
1. 业务代码 try/catch/throw 统计(Grep count)
try { → 业务代码(route/、model/)中 0 处,仅在 xiunophp 库层与 js 文件
throw new → 业务代码 0 处(仅 xiunophp/xn_zip_old.func.php 3 处、xn_send_mail 21 处)
catch ( → 业务代码 0 处
trigger_error → 15 处 / 9 个文件(散落在 xiunophp 库类)
2. xn_error 用 global 变量记录错误,嵌套调用会被覆盖
文件:xiunobbs_4.0.4/xiunophp/misc.func.php 第 56-61 行
function xn_error($no, $str, $return = FALSE) {
global $errno, $errstr;
$errno = $no;
$errstr = $str;
return $return;
}
调用方模式(db.func.php 第 17-22 行):
if(!$db || ($db && $db->errstr)) {
$errno = -1;
$errstr = $db->errstr;
return FALSE;
}
3. db_errno_errstr 写 global 错误变量,调用链丢失
文件:xiunobbs_4.0.4/xiunophp/db.func.php 第 201-209 行
function db_errno_errstr($r, $d = NULL, $sql = '') {
global $errno, $errstr;
if($r === FALSE) {
$errno = $d->errno;
$errstr = db_errstr_safe($errno, $d->errstr);
$s = 'SQL:'.$sql."\r\nerrno: ".$errno.", errstr: ".$errstr;
xn_log($s, 'db_error');
}
}
业务调用方仅能 if ($r === FALSE) message(-1, $errstr) 读取最新一次错误,嵌套调用上层错误已被覆盖。
4. error_handle 仅在 DEBUG>0 时生效,线上错误被静默
文件:xiunobbs_4.0.4/xiunophp/misc.func.php 第 21-53 行
function error_handle($errno, $errstr, $errfile, $errline) {
if(DEBUG == 0) return FALSE;
$subject = "Error[$errno]: $errstr, File: $errfile, Line: $errline";
xn_log($subject, 'php_error');
echo ($ajax || IN_CMD) ? $txt : $html;
DEBUG == 2 AND xn_log($txt, 'debug_error');
return TRUE;
}
文件:xiunobbs_4.0.4/xiunophp/xiunophp.php 第 80 行
DEBUG AND set_error_handler('error_handle', -1);
线上 DEBUG=0 时 set_error_handler 不注册,PHP 错误按默认行为(写 error_log 后吞掉),业务层无感知。
5. message() 函数直接 exit,无法被 try/catch 捕获
文件:xiunobbs_4.0.4/model/misc.func.php 第 92-114 行
function message($code, $message, $extra = array()) {
static $called = FALSE;
$called ? exit(xn_json_encode($arr)) : $called = TRUE;
if($ajax) {
echo xn_json_encode($arr);
} else {
}
exit;
}
业务代码大量使用 empty($email) AND message('email', lang('email_is_empty')),等同于 if (...) { exit; },无法被外层 try/catch 接管做统一日志/告警/事务回滚。
6. 业务代码典型错误处理:返回 FALSE + global errstr
文件:xiunobbs_4.0.4/route/post.php 第 71 行
$pid = post_create($post, $fid, $gid);
empty($pid) AND message(-1, lang('create_post_failed'));
post_create 内部失败时 $errstr 已被覆盖,但调用方只 message(-1, lang('create_post_failed')) 显示固定文案,不读取 $errstr,错误信息彻底丢失。
风险等级与结论
架构缺陷(严重)
危害:
- 业务代码 0 try/catch,任何运行时错误都直接终止流程,无法优雅降级
- 全局
$errno/$errstr 在嵌套调用中被覆盖,错误源头不可追溯
message() 直接 exit,事务中调用会导致事务未回滚、连接未释放
- 线上
DEBUG=0 时 error_handle 不注册,PHP 错误被静默吞掉,故障难定位
- 错误信息丢失:业务调用方常用固定文案
message(-1, lang('xxx_failed')) 而非读取 $errstr
- 无统一异常处理器,无法集中收集错误上下文(请求参数、用户、SQL、调用栈)
修复建议:
- 引入业务异常类
BusinessException、SystemException,message() 改为抛异常
- 全局注册
set_exception_handler 与 set_error_handler(无论 DEBUG 值),统一记录日志 + 格式化响应
- 废弃
xn_error + global 模式,函数返回 Result 对象或抛异常
- 事务边界包裹 try/catch,确保异常时回滚