此文章为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)."'";
$s .= "`$k`=$v AND ";
} 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 ";
}
}
}
$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);
$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