Xiuno BBS 审计之问题:无 ORM,裸 SQL 防注入难
贰先生 6小时前

此文章为XIUNOX版本重构审计时发现问题,XIUNOX版本已优化修复此问题。分享出来方便后续想基于xiuno bbs4.0.4版本制作维护版本或插件模板等需求的开发者和站长参考。

现象

Xiuno BBS 4.0.4 数据访问层无 ORM、无 Query Builder、无预处理语句(Prepared Statement),全部通过 db_cond_to_sqladd()db_array_to_insert_sqladd()db_array_to_update_sqladd() 等函数将 PHP 数组拼接为 SQL 字符串。防 SQL 注入仅依赖 addslashes() 函数(而非 mysql_real_escape_string 或 PDO 参数绑定),且拼接逻辑中存在多处在数组元素上调用 addslashes 后又判断 is_int/is_float 的低效写法。表名、字段名直接拼接(仅用 addslashes($k) + 反引号包裹),无法防字段名注入。

源码证据

1. db_cond_to_sqladd 用 addslashes 拼接 WHERE 条件

文件:xiunobbs_4.0.4/xiunophp/db.func.php 第 242-279 行

function db_cond_to_sqladd($cond) {
    $s = '';
    if(!empty($cond)) {
        $s = ' WHERE ';
        foreach($cond as $k=>$v) {
            if(!is_array($v)) {
                $v = (is_int($v) || is_float($v)) ? $v : "'".addslashes($v)."'";   // addslashes 防注入
                $s .= "`$k`=$v AND ";                                               // 字段名 $k 未转义
            } elseif(isset($v[0])) {
                $s .= '(';
                foreach ($v as $v1) {
                    $v1 = (is_int($v1) || is_float($v1)) ? $v : "'".addslashes($v1)."'";
                    $s .= "`$k`=$v1 OR ";
                }
                $s = substr($s, 0, -4);
                $s .= ') AND ';
            } else {
                foreach($v as $k1=>$v1) {
                    if($k1 == 'LIKE') {
                        $k1 = ' LIKE ';
                        $v1="%$v1%";
                    }
                    $v1 = (is_int($v1) || is_float($v1)) ? $v1 : "'".addslashes($v1)."'";
                    $s .= "`$k`$k1$v1 AND ";                                         // 操作符 $k1 未校验
                }
            }
        }
        $s = substr($s, 0, -4);
    }
    return $s;
}

2. db_array_to_update_sqladd 对 value 重复 addslashes

文件:xiunobbs_4.0.4/xiunophp/db.func.php 第 303-318 行

function db_array_to_update_sqladd($arr) {
    $s = '';
    foreach($arr as $k=>$v) {
        $v = addslashes($v);                                          // 先 addslashes
        $op = substr($k, -1);
        if($op == '+' || $op == '-') {
            $k = substr($k, 0, -1);
            $v = (is_int($v) || is_float($v)) ? $v : "'$v'";          // 再判断类型,逻辑错乱
            $s .= "`$k`=$k$op$v,";
        } else {
            $v = (is_int($v) || is_float($v)) ? $v : "'$v'";
            $s .= "`$k`=$v,";
        }
    }
    return substr($s, 0, -1);
}

3. db_array_to_insert_sqladd 字段名与值均 addslashes

文件:xiunobbs_4.0.4/xiunophp/db.func.php 第 327-342 行

function db_array_to_insert_sqladd($arr) {
    $s = '';
    $keys = array();
    $values = array();
    foreach($arr as $k=>$v) {
        $k = addslashes($k);
        $v = addslashes($v);
        $keys[] = '`'.$k.'`';
        $v = (is_int($v) || is_float($v)) ? $v : "'$v'";
        $values[] = $v;
    }
    $keystr = implode(',', $keys);
    $valstr = implode(',', $values);
    $sqladd = "($keystr) VALUES ($valstr)";
    return $sqladd;
}

4. db_insert/db_update/db_delete 直接拼接 SQL 字符串执行

文件:xiunobbs_4.0.4/xiunophp/db.func.php 第 122-160 行

function db_insert($table, $arr, $d = NULL) {
    $db = $_SERVER['db'];
    $d = $d ? $d : $db;
    if(!$d) return FALSE;
    $sqladd = db_array_to_insert_sqladd($arr);
    if(!$sqladd) return FALSE;
    return db_exec("INSERT INTO {$d->tablepre}$table $sqladd", $d);    // 字符串拼接
}

function db_update($table, $cond, $update, $d = NULL) {
    $db = $_SERVER['db'];
    $d = $d ? $d : $db;
    if(!$d) return FALSE;
    $condadd = db_cond_to_sqladd($cond);
    $sqladd = db_array_to_update_sqladd($update);
    if(!$sqladd) return FALSE;
    return db_exec("UPDATE {$d->tablepre}$table SET $sqladd $condadd", $d);
}

function db_delete($table, $cond, $d = NULL) {
    $db = $_SERVER['db'];
    $d = $d ? $d : $db;
    if(!$d) return FALSE;
    $condadd = db_cond_to_sqladd($cond);
    return db_exec("DELETE FROM {$d->tablepre}$table $condadd", $d);
}

5. db_pdo_mysql 类存在但仍用字符串拼接而非 prepared statement

文件:xiunobbs_4.0.4/xiunophp/db_pdo_mysql.class.php(Grep 显示 3 处 try/catch、3 处 trigger_error,但仍接收 SQL 字符串执行)

db_exec() 接收完整 SQL 字符串,PDO 类内部用 query() 执行而非 prepare() + bindParam(),未利用 PDO 的预处理能力。

6. 业务层直接拼 SQL 的例子

文件:xiunobbs_4.0.4/model/forum.func.php(Grep 显示 5 处 global、36 处 hook,含直接 SQL)

业务层虽通过 db_find/db_find_one 抽象,但条件数组中的字段名 $k 直接来自业务代码(如 array('fid'=>$fid)),若 $k 被外部可控则字段名注入。

风险等级与结论

架构缺陷(严重)

危害:

  • addslashes 在多字节字符集(如 GBK)下可被绕过,MySQL 真正的转义函数应为 mysql_real_escape_string 或 PDO 参数绑定
  • 字段名 $k 仅用反引号包裹 + addslashes,未做白名单校验,外部可控时存在字段名注入风险
  • 操作符 $k1(如 LIKE><)未做白名单,理论可注入
  • 无 ORM 导致 SQL 与业务代码强耦合,无法切换数据库类型(虽然支持 pdo_sqlite/pdo_mongodb 但 SQL 语法差异未抽象)
  • 无预处理语句导致每次 SQL 都需重新解析计划,无法利用 prepared statement 缓存
  • db_array_to_update_sqladd  addslashes 后再判断 is_int 逻辑错乱(addslashes 后的字符串不会被 is_int 识别),存在边缘 bug
  • 表关联、事务、嵌套查询无封装,业务代码需手写大量 SQL

修复建议:

  • 引入 Query Builder(如 db_table('user')->where('uid', $uid)->first()),生成参数化 SQL
  • 字段名白名单校验(从表结构元数据获取合法字段)
  • 切换为 PDO prepared statement,所有值通过 bindParam 绑定
  • 引入轻量 ORM(如 Eloquent 风格),封装关联关系(hasMany/belongsTo)
  • 表名/字段名转义使用 addcslashes($k, "\")` 配合白名单
  • 废弃 addslashes,改用 mysqli_real_escape_string 或 PDO bind
最新回复 (0)
全部楼主
返回