此文章为XIUNOX版本重构审计时发现问题,XIUNOX版本已优化修复此问题。分享出来方便后续想基于xiuno bbs4.0.4版本制作维护版本或插件模板等需求的开发者和站长参考。
现象
Xiuno BBS 4.0.4 通过 <!--{hook xxx}--> / // hook xxx 标记在源文件中插入钩子点,多插件可同时挂载同一钩子。系统提供 hooks_rank 字段用于排序,但存在严重设计缺陷:
- 默认 rank 为 0,多个插件未声明 rank 时全部并列 0,最终顺序由
glob() 返回顺序决定(受文件系统影响,不确定)。
- 当两个插件声明相同 rank(如都设 10)时,无任何仲裁机制,执行顺序不可预期。
- 钩子内容直接被字符串拼接进编译后文件,多插件同时修改同一变量(如
$user、$thread)会互相覆盖,无隔离。
- rank 仅影响顺序,无法表达"独占钩子""互斥挂载"等约束。
源码证据
文件:xiunobbs_4.0.4/model/plugin.func.php
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);
}
}
}
foreach ($hooks as $hookname=>$arrlist) {
$arrlist = arrlist_multisort($arrlist, 'rank', FALSE);
$hooks[$hookname] = arrlist_values($arrlist, 'hookpath');
}
}
$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;
}
文件:xiunobbs_4.0.4/model/plugin.func.php
!isset($local['hooks_rank']) && $local['hooks_rank'] = array();
风险等级与结论
风险等级:高(High)|生态缺陷
危害:
- 多插件同钩子无仲裁,安装顺序、文件系统顺序决定执行顺序,行为不可重现。
- 钩子内容直接拼接为字符串,两个插件若同时修改
$thread['subject'] 或 $user 等共享变量,会引发静默数据覆盖,难以排查。
- 没有钩子优先级冲突检测,安装新插件时不会警告"该钩子已被另一插件占用且 rank 相同"。
- 插件作者无法声明"我必须最后执行""我必须最先执行""此钩子互斥"等语义,生态协作困难。
修复建议:
- 默认 rank 改为按插件安装顺序自动递增(如
installed_index * 100),未声明时仍有可预期顺序。
- 同 rank 冲突时,引入二级排序键(插件 dir 字典序),保证可重现。
- 在
plugin_compile_srcfile_callback 中检测同 hook 多挂载情况,记录到日志或运行时警告。
- 提供"钩子独占"声明字段
hooks_exclusive,安装时若发现冲突直接拒绝。
- 引入钩子命名空间隔离,例如自动包裹
function plugin_xxx_hookname() { ... },避免变量污染。