此文章为XIUNOX版本重构审计时发现问题,XIUNOX版本已优化修复此问题。分享出来方便后续想基于xiuno bbs4.0.4版本制作维护版本或插件模板等需求的开发者和站长参考。
现象
Xiuno BBS 4.0.4 的插件机制采用"源码合并编译"方式:在 plugin_compile_srcfile() 中通过正则匹配 // hook xxx 标记,将插件 hook/*.php 文件内容直接拼接进宿主源文件,然后写入 tmp/ 目录作为编译产物 include 执行。整个过程无 try/catch 包裹、无错误隔离、无插件依赖检查、无运行时降级,任意一个插件的语法错误、致命错误或未捕获异常都会让整页(甚至整站,因为 model.min.php 也走同一编译流程)直接白屏。
源码证据
1. plugin_compile_srcfile:源码字符串拼接,无任何隔离
文件:xiunobbs_4.0.4/model/plugin.func.php 第 282-304 行
function plugin_compile_srcfile($srcfile) {
global $conf;
if(!empty($conf['disabled_plugin'])) {
$s = file_get_contents($srcfile);
return $s;
}
$srcfile = plugin_find_overwrite($srcfile);
$s = file_get_contents($srcfile);
for($i = 0; $i < 10; $i++) {
if(strpos($s, '<!--{hook') !== FALSE || strpos($s, '// hook') !== FALSE) {
$s = preg_replace('#<!--{hook\s+(.*?)}-->#', '// hook \\1', $s);
$s = preg_replace_callback('#//\s*hook\s+(\S+)#is', 'plugin_compile_srcfile_callback', $s);
} else {
break;
}
}
return $s;
}
2. plugin_compile_srcfile_callback:直接 file_get_contents 拼接,无校验
文件:xiunobbs_4.0.4/model/plugin.func.php 第 342-389 行
function plugin_compile_srcfile_callback($m) {
static $hooks;
if(empty($hooks)) {
$hooks = array();
$plugin_paths = plugin_paths_enabled();
foreach($plugin_paths as $path=>$pconf) {
$dir = file_name($path);
$hookpaths = glob(APP_PATH."plugin/$dir/hook/*.*");
if(is_array($hookpaths)) {
foreach($hookpaths as $hookpath) {
$hookname = file_name($hookpath);
$rank = isset($pconf['hooks_rank']["$hookname"]) ? $pconf['hooks_rank']["$hookname"] : 0;
$hooks[$hookname][] = array('hookpath'=>$hookpath, 'rank'=>$rank);
}
}
}
}
$s = '';
$hookname = $m[1];
if(!empty($hooks[$hookname])) {
$fileext = file_ext($hookname);
foreach($hooks[$hookname] as $path) {
$t = file_get_contents($path);
if($fileext == 'php' && preg_match('#^\s*<\?php\s+exit;#is', $t)) {
$t = preg_replace('#^\s*<\?php\s*exit;(.*?)(?:\?>)?\s*$#is', '\\1', $t);
}
$s .= $t;
}
}
return $s;
}
3. 编译产物直接 include,无 try/catch、无错误捕获
文件:xiunobbs_4.0.4/model/plugin.func.php 第 15-37 行
function _include($srcfile) {
global $conf;
$len = strlen(APP_PATH);
$tmpfile = $conf['tmp_path'].substr(str_replace('/', '_', $srcfile), $len);
if(!is_file($tmpfile) || DEBUG > 1) {
$s = plugin_compile_srcfile($srcfile);
$g_include_slot_kv = array();
for($i = 0; $i < 10; $i++) {
$s = preg_replace_callback('#<template\sinclude="(.*?)">(.*?)</template>#is', '_include_callback_1', $s);
if(strpos($s, '<template') === FALSE) break;
}
file_put_contents_try($tmpfile, $s);
$s = plugin_compile_srcfile($tmpfile);
file_put_contents_try($tmpfile, $s);
}
return $tmpfile;
}
调用方(如 index.inc.php 第 59 行):
case 'user': include _include(APP_PATH.'route/user.php'); break;
include 编译后的 tmp 文件,无任何错误隔离层。
4. model.inc.php 同样走编译流程,model.min.php 全局合并放大故障半径
文件:xiunobbs_4.0.4/model.inc.php 第 41-62 行
if(DEBUG) {
foreach ($include_model_files as $model_files) {
include _include($model_files);
}
} else {
$model_min_file = $conf['tmp_path'].'model.min.php';
$isfile = is_file($model_min_file);
if(!$isfile) {
$s = '';
foreach($include_model_files as $model_files) {
$t = file_get_contents(_include($model_files));
$t = trim($t);
$t = ltrim($t, '<?php');
$t = rtrim($t, '?>');
$s .= "<?php\r\n".$t."\r\n?>";
}
$r = file_put_contents($model_min_file, $s);
unset($s);
}
include $model_min_file;
}
任何 model 钩子中的插件代码语法错误都会让 model.min.php 编译失败,全站业务瘫痪。
风险等级与结论
架构缺陷(严重)
危害:
- 单个插件的 PHP 语法错误(如少一个分号、未闭合括号)会让编译产物整体语法错误,整页白屏
- 单个插件运行时致命错误(如调用未定义函数、内存溢出)无 try/catch 兜底,错误直接抛给用户
model.min.php 全局合并放大故障半径,一个 model 钩子插件出错全站瘫痪
- 无插件依赖检查(仅
plugin_dependencies 函数声明但 plugin_install 未调用),缺依赖插件加载即报错
- 无插件熔断/降级机制,问题插件持续影响主站,只能手工删除
tmp/ 目录或禁用插件
- 编译产物缓存于
tmp/,DEBUG=0 时不会自动重建,调试困难
修复建议:
- 改为运行时事件分发器模式(如
Hook::trigger('user_login_post_start')),插件以闭包/类注册回调,单插件异常被 try/catch 捕获并记录日志后继续
- 引入插件健康度检测:插件加载失败超过阈值自动禁用并通知管理员
- 编译期增加
php -l 语法检查,避免语法错误写入 tmp
- 为
_include 增加 fallback:编译失败时回退到无插件源码并记录告警