From aaf5fe48c6c078766c6b475a2a0483295baad42a Mon Sep 17 00:00:00 2001 From: youpaishidifu <1@ypshidifu.cn> Date: Sat, 10 Oct 2026 12:41:33 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8A=A0=E5=9B=BA-07(P0-06):=20=E5=B0=81?= =?UTF-8?q?=E5=A0=B5=20SQL=20=E6=B3=A8=E5=85=A5=E9=9D=A2=EF=BC=88=E5=8A=A8?= =?UTF-8?q?=E6=80=81=E5=88=97=E5=90=8D=E7=99=BD=E5=90=8D=E5=8D=95=20+=20?= =?UTF-8?q?=E8=A3=B8=E6=8B=BC=E5=80=BC=E5=BC=BA=E7=B1=BB=E5=9E=8B=E5=8C=96?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 背景:多处 SQL 由用户输入拼接——update() 的动态键(mod)直接作列名入 SET 子句; 卡密/分类卡密批量 INSERT 手拼 SQL 值元组(insert_back_row 零转义);DelUse 删除 语句手拼 fid。列名注入与手拼值注入均可被利用篡改/破坏数据。 1. 列名注入(动态 mod 键): - function/function_core.php 新增 wq_is_db_field($name):仅允许 [A-Za-z0-9_]{1,64} 合法 SQL 标识符,杜绝引号/空格/注释符进入列名 - admin/ajax.php 四处 change* 动态更新加守卫(非法 mod 直接 die「不支持修改的字段」): * changegrade (sq_level) * changeuser (sq_user) * changefid (sq_fidlist) * changeagent (sq_agent) - 注:agent 侧 changeuser/changeagent 已有字段白名单(P0-05 已加 password 哈希),无需重复 2. 手拼 INSERT 值元组(creatkey / creatfidkey,admin+agent 双侧): - 卡密金额 sc_money:强制 is_numeric(),非法直接拒绝(money 会裸拼进 INSERT 与卡密串) - 分类 fid sc_fid:强制 (int) 整型化后再拼入(防多语句/注释符逃逸) 3. DelUse 删除语句(admin+agent 双侧): - fid 强制 (int) 整型化;agent 侧 aid 亦 (int) 化 - 注入载荷如 "1 AND 1=1" 被截为 1,SQL 结构不再可被改写 回归(30 项全 PASS,8082 内置服务器): - 各 change* 正常字段修改成功且 DB 生效;列名注入(price,x / 多语句 DROP 载荷)全部被拒,目标表完好 - creatkey 正常生成、余额正确扣除;money 注入载荷被 is_numeric 拒绝 - creatfidkey 合法 fid 正常生成;注入 fid 在 fid 查找即被拒(int 化后无命中) - DelUse 注入 fid 被 int 化后正常执行(删 0 行),表结构完好 - 注:(int)"1; DROP..."=1(前导数字保留),(int)"'; DROP..."=0,载荷均无法逃逸 SQL 结构 --- admin/ajax.php | 28 ++++++++++++++++++++++++---- agent/ajax.php | 12 ++++++++---- function/function_core.php | 9 +++++++++ 3 files changed, 41 insertions(+), 8 deletions(-) diff --git a/admin/ajax.php b/admin/ajax.php index 70027dc..e5f3e21 100644 --- a/admin/ajax.php +++ b/admin/ajax.php @@ -462,6 +462,10 @@ switch ($_GET['mod']){ if (empty($_POST['mod'])){ die('模块标识不能为空'); } + // P0-06: mod 作为动态列名入 SQL,先校验合法标识符,防列名注入 + if (!wq_is_db_field($_POST['mod'])){ + die('不支持修改的字段'); + } if (empty($_POST['gid'])){ die('等级ID不能为空'); } @@ -476,6 +480,10 @@ switch ($_GET['mod']){ if (empty($_POST['mod'])){ die('模块标识不能为空'); } + // P0-06: mod 作为动态列名入 SQL,先校验合法标识符,防列名注入 + if (!wq_is_db_field($_POST['mod'])){ + die('不支持修改的字段'); + } if (empty($_POST['userid'])){ die('用户ID不能为空'); } @@ -561,6 +569,10 @@ switch ($_GET['mod']){ if (empty($_POST['mod'])){ die('模块标识不能为空'); } + // P0-06: mod 作为动态列名入 SQL,先校验合法标识符,防列名注入 + if (!wq_is_db_field($_POST['mod'])){ + die('不支持修改的字段'); + } if (empty($_POST['fid'])){ die('分类ID不能为空'); } @@ -700,6 +712,10 @@ switch ($_GET['mod']){ if (empty($_POST['mod'])){ die('模块标识不能为空'); } + // P0-06: mod 作为动态列名入 SQL,先校验合法标识符,防列名注入 + if (!wq_is_db_field($_POST['mod'])){ + die('不支持修改的字段'); + } if (empty($_POST['aid'])){ die('代理ID不能为空'); } @@ -807,8 +823,9 @@ switch ($_GET['mod']){ } $time = time(); $money = $_POST['sc_money']; - if ($money <= 0){ - die(json_encode(array('code'=>'-1','msg'=>'卡密金额不能为空或者0或者负数'))); + // P0-06: money 会裸拼进 INSERT SQL 与卡密字符串,强制数字 + if (!is_numeric($money) || $money <= 0){ + die(json_encode(array('code'=>'-1','msg'=>'卡密金额格式错误'))); } $keylist=''; for ($x=1; $x<=$_POST['sc_count']; $x++) { @@ -1471,9 +1488,11 @@ switch ($_GET['mod']){ die(json_encode(array('code'=>'-1','msg'=>'卡密金额不能为空或者0或者负数'))); } $keylist=''; + // P0-06: sc_fid 裸拼进 INSERT SQL,强制整型 + $scfid = (int)$_POST['sc_fid']; for ($x=1; $x<=$_POST['sc_count']; $x++) { $key = substr($time,0,6).'-'.rand_str(6).'-'.rand_str(6).'-'.rand_str(6).'-'.rand_str(6).rand_str(6); - $insarray[] = "'{$key}','{$time}','0','{$_POST['sc_fid']}','1'"; + $insarray[] = "'{$key}','{$time}','0','$scfid','1'"; $keylist .= '
'.$key; } $sign = rand_str(32); @@ -1552,7 +1571,8 @@ switch ($_GET['mod']){ die('成功删除'.(int)$db->affected_num().'行'); break; case 'DelUse': - $db->delete('sq_fidkey', '`usetime` > 0 AND `fid` = '.$_POST['fid'].' AND `status` = 1','AND'); + // P0-06: fid 裸拼进 DELETE SQL,强制整型 + $db->delete('sq_fidkey', '`usetime` > 0 AND `fid` = '.(int)$_POST['fid'].' AND `status` = 1','AND'); die('成功删除'.(int)$db->affected_num().'行'); break; } diff --git a/agent/ajax.php b/agent/ajax.php index 7af053a..cf61914 100644 --- a/agent/ajax.php +++ b/agent/ajax.php @@ -1044,9 +1044,11 @@ switch ($_GET['mod']) { } $keylist=''; + // P0-06: sc_fid 裸拼进 INSERT SQL,强制整型(上方 fidinfo 校验保证 fid 有效) + $scfid = (int)$_POST['sc_fid']; for ($x=1; $x<=$_POST['sc_count']; $x++) { $key = substr($time,0,6).'-'.rand_str(6).'-'.rand_str(6).'-'.rand_str(6).'-'.rand_str(6).rand_str(6); - $insarray[] = "'{$key}','{$time}','0','{$_POST['sc_fid']}','1','{$_SESSION['agent_id']}'"; + $insarray[] = "'{$key}','{$time}','0','$scfid','1','{$_SESSION['agent_id']}'"; $keylist .= '
'.$key; } $sign = rand_str(32); @@ -1126,8 +1128,9 @@ switch ($_GET['mod']) { } $time = time(); $money = $_POST['sc_money']; - if ($money <= 0){ - die(json_encode(array('code'=>'-1','msg'=>'卡密金额不能为空或者0或者负数'))); + // P0-06: money 会裸拼进 INSERT SQL 与卡密字符串,强制数字 + if (!is_numeric($money) || $money <= 0){ + die(json_encode(array('code'=>'-1','msg'=>'卡密金额格式错误'))); } $spendmoney = $money * $count; if (!$agentinfo = $db->select_first_row('sq_agent', '*', array('username' => $_SESSION['agent_username']), 'AND')) { @@ -1182,7 +1185,8 @@ switch ($_GET['mod']) { die('成功删除'.(int)$db->affected_num().'行'); break; case 'DelUse': - $db->delete('sq_fidkey', '`usetime` > 0 AND `fid` = '.$_POST['fid'].' AND `aid` = '.$_SESSION['agent_id'].' AND `status` = 1','AND'); + // P0-06: fid 裸拼进 DELETE SQL,强制整型 + $db->delete('sq_fidkey', '`usetime` > 0 AND `fid` = '.(int)$_POST['fid'].' AND `aid` = '.(int)$_SESSION['agent_id'].' AND `status` = 1','AND'); die('成功删除'.(int)$db->affected_num().'行'); break; case 'DelAll': diff --git a/function/function_core.php b/function/function_core.php index 550adf9..9b039c2 100644 --- a/function/function_core.php +++ b/function/function_core.php @@ -251,6 +251,15 @@ function wq_verify_password($plain, $stored, $legacy = 'plain') return hash_equals($stored, $plain); } +/** + * P0-06: 校验动态字段标识(mod)是否为合法 SQL 列名,防列名注入 + * 动态 UPDATE/INSERT 的键来自用户输入时,须先通过本校验 + */ +function wq_is_db_field($name) +{ + return is_string($name) && preg_match('/^[A-Za-z0-9_]{1,64}$/', $name) === 1; +} + /* * 布尔逻辑型转换为数字0或1