此文章为XIUNOX版本重构审计时发现问题,XIUNOX版本已优化修复此问题。分享出来方便后续想基于xiuno bbs4.0.4版本制作维护版本或插件模板等需求的开发者和站长参考。
现象
Xiuno BBS 4.0.4 自定义 session 处理器(sess_open/sess_read/sess_write/sess_destroy/sess_gc)将会话数据存入 bbs_session 表,覆盖了 PHP 默认文件 session 机制。虽然解决了多机部署 session 共享问题,但将会话压力全部转移到 MySQL 主库(无独立 session 服务器配置),未提供 Redis/Memcached session 存储选项。代码注释中提到"可以指定独立的 session 服务器"但实际为注释掉的 $g_sess_db = $db,未实现。Session 写入采用全量 update 模式,高并发下成为瓶颈。
源码证据
1. session.func.php 注释承认 PHP 默认文件 session 的缺陷,但改用 db 后未解决分布式
文件:xiunobbs_4.0.4/model/session.func.php 第 1-15 行
<?php
php 默认的 session 采用文件存储,并且使用 flock() 文件锁避免并发访问不出问题(实际上还是无法解决业务层的并发读后再写入)
自定义的 session 采用数据表来存储,同样无法解决业务层并发请求问题。
xiuno.js $.each_sync() 串行化并发请求,可以避免客户端并发访问导致的 session 写入问题。
*/
$sid = '';
$g_session = array();
$g_session_invalid = FALSE;
最后一行注释承认"可以指定独立的 session 服务器"但未实现,$g_sess_db 始终为主 $db。
2. sess_read 直接读 bbs_session 表,无独立连接
文件:xiunobbs_4.0.4/model/session.func.php 第 29-52 行
function sess_read($sid) {
global $g_session, $longip, $time;
if(empty($sid)) {
$sid = session_id();
sess_new($sid);
return '';
}
$arr = db_find_one('session', array('sid'=>$sid));
if(empty($arr)) {
sess_new($sid);
return '';
}
if($arr['bigdata'] == 1) {
$arr2 = db_find_one('session_data', array('sid'=>$sid));
$arr['data'] = $arr2['data'];
}
$g_session = $arr;
return $arr ? $arr['data'] : '';
}
3. sess_write 全量 update,无独立 session 服务器
文件:xiunobbs_4.0.4/model/session.func.php 第 106-169 行
function sess_write($sid, $data) {
global $g_session, $time, $longip, $g_session_invalid, $conf;
$uid = _SESSION('uid');
$fid = _SESSION('fid');
unset($_SESSION['uid']);
unset($_SESSION['fid']);
if($data) {
$data = session_encode();
}
$arr = array(
'uid'=>$uid,
'fid'=>$fid,
'url'=>$url,
'last_date'=>$time,
'data'=> $data,
'ip'=> $longip,
'useragent'=> $agent,
'bigdata'=> 0,
);
$session_delay_update_on = !empty($conf['session_delay_update']) && $time - $g_session['last_date'] < $conf['session_delay_update'];
if($session_delay_update_on) {
unset($arr['fid']);
unset($arr['url']);
unset($arr['last_date']);
}
$len = strlen($data);
if($len > 255 && $g_session['bigdata'] == 0) {
db_insert('session_data', array('sid'=>$sid));
}
if($len <= 255) {
$update = array_diff_value($arr, $g_session);
db_update('session', array('sid'=>$sid), $update);
} else {
db_update('session_data', array('sid'=>$sid), $update2);
}
return TRUE;
}
4. sess_start 设置 cookie 参数,但 session 存储仍为 db
文件:xiunobbs_4.0.4/model/session.func.php 第 187-220 行
function sess_start() {
global $conf, $sid, $g_session;
ini_set('session.name', 'bbs_sid');
ini_set('session.use_cookies', 'On');
ini_set('session.cookie_lifetime', 86400);
ini_set('session.cookie_httponly', 'On');
ini_set('session.gc_maxlifetime', $conf['online_hold_time']);
session_set_save_handler('sess_open', 'sess_close', 'sess_read', 'sess_write', 'sess_destroy', 'sess_gc');
register_shutdown_function('session_write_close');
session_start();
$sid = session_id();
return $sid;
}
5. 配置层未提供 session 存储选项
文件:xiunobbs_4.0.4/conf/conf.default.php 中无 session 配置项,仅 cache 与 db。session 存储实现完全硬编码在 model/session.func.php,无法通过配置切换为 Redis/Memcached。
6. sess_gc 全表扫描删除,大表性能差
文件:xiunobbs_4.0.4/model/session.func.php 第 178-185 行
function sess_gc($maxlifetime) {
global $time;
$expiry = $time - $maxlifetime;
db_delete('session', array('last_date'=>array('<'=>$expiry)));
db_delete('session_data', array('last_date'=>array('<'=>$expiry)));
return TRUE;
}
GC 概率为 1/500(ini_set('session.gc_divisor', 500)),每次 GC 触发全表 DELETE,session 表数据量大时锁表风险高。
风险等级与结论
架构缺陷(中)
危害:
- Session 存储与业务库共用 MySQL 主库,session 读写压力直接拖垮业务库
- 注释提到的"独立 session 服务器"未实现,无法横向扩展
- 无 Redis/Memcached session 存储选项,与缓存层支持丰富后端形成对比
- sess_write 全量 update(uid/fid/url/last_date/data/ip/useragent/bigdata),高并发下行锁竞争
- sess_gc 全表 DELETE,大表锁表风险
- 多机部署时所有 session 读写集中到主库,无读写分离
修复建议:
- 配置层增加
session 配置项,支持 type: db/redis/memcached/file
- 实现
session_redis 类,将会话存储迁移至 Redis(TTL 自动过期,无需 GC)
- sess_write 改为增量 update(仅写变更字段)
- 引入 session 独立连接池,与业务 db 隔离
- sess_gc 改为按 sid 哈希分批删除,避免全表锁