Xiuno BBS 审计之问题:会话存储缺陷,依赖 DB 单点
贰先生 6小时前

此文章为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;

最后一行注释承认"可以指定独立的 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));        // 用主 $db
    if(empty($arr)) {
        sess_new($sid);
        return '';
    }
    if($arr['bigdata'] == 1) {
        $arr2 = db_find_one('session_data', array('sid'=>$sid));  // 大 session 拆表
        $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 延迟更新,减轻压力
    $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));         // 主 $db 写
    }
    if($len <= 255) {
        $update = array_diff_value($arr, $g_session);
        db_update('session', array('sid'=>$sid), $update);     // 主 $db 写
        // ...
    } else {
        // ...
        db_update('session_data', array('sid'=>$sid), $update2);  // 主 $db 写
    }
    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)));        // 全表 DELETE
    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 哈希分批删除,避免全表锁
最新回复 (0)
全部楼主
返回