二次sql注入本质是数据重入执行,即从数据库读出的数据未经校验直接拼入新sql语句;校验必须在sql构造前、紧贴执行逻辑处进行,且须用白名单或强类型约束,参数化无法覆盖动态表名等场景。

二次SQL注入的本质是“数据重入执行”
不是所有从数据库读出来的数据都安全,关键看它后续会不会被拼进新的SQL语句。比如用户注册时存了 admin' OR '1'='1,当时没出问题;但某天后台用这个字段拼了 SELECT * FROM logs WHERE user = '{$row['name']}',就炸了。校验不是为了“消毒”,而是为了阻断“信任链”——数据库字段 ≠ 可信输入。
校验必须在拼接SQL前做,不能靠入库时过滤
入库时转义或参数化只能防一次注入,对二次注入无效。真正要拦住的,是读出来之后、进 mysqli_query 或 PDO::exec 之前那一步。常见错误是只在表单提交时校验,却放任 $user_name 从DB查出来后直接进查询。
- 校验位置:紧贴SQL构造逻辑,比如在调用
prepare()前检查$name是否含'、;、UNION等危险字符 - 不要用黑名单(如简单
str_replace(['"', "'"], '', $s)),容易绕过;优先用白名单(如只允许字母数字下划线)或类型强约束(如ID必须是is_int()或ctype_digit()) - 如果字段本该是邮箱,就用
filter_var($email, FILTER_VALIDATE_EMAIL);如果是用户名,用preg_match('/^[a-zA-Z0-9_\x{4e00}-\x{9fa5}]{2,20}$/u', $name)
参数化无法自动覆盖二次注入场景
很多人以为用了PDO预处理就万事大吉,其实不然。预处理只保护“你写的SQL模板里的占位符”,不保护“你动态拼出来的表名、字段名、ORDER BY 子句”。比如:
SELECT * FROM {$table} WHERE id = ?
这里 $table 是从DB读出来的,哪怕id用?占位,$table 依然能触发注入。这种地方必须单独校验 $table 是否在允许列表里(如 in_array($table, ['users', 'orders']))。
- 危险函数组合:任何把DB字段直接塞进
mysql_query("...{$var}...")、mysqli_real_escape_string()(已废弃)、或eval()/create_function()的地方都要拉响警报 -
mysqli_real_escape_string()对二次注入基本无效——它依赖当前连接的字符集,而读取字段时连接可能已切换,逃逸失效 - ORM如Eloquent默认不保护动态表名/列名,
whereRaw()、orderByRaw()同样需要手动校验传入的变量
别把校验逻辑写死在业务代码里
每次读DB后都手写 if (!ctype_alnum($name)) die('hacker') 很容易漏。更可靠的方式是定义“可信上下文”:
- 封装读取函数,如
getTrustedUserName($id),内部强制走白名单校验,业务层只许调它,不许直查users.name - 对高危字段(如
redirect_url、callback)加注释标记:// DB field: must be validated before use in header() or SQL - 用PHP的
filter_var()配合自定义过滤器,比如为SQL标识符写一个FILTER_SANITIZE_STRING+ 正则限制的组合
最麻烦的其实是那些“看起来无害”的字段:昵称、签名、来源URL——它们往往被当成普通字符串显示,结果某天被拿去拼 INSERT INTO audit_log (op_by) VALUES (?),而?前面那个 audit_log 表名又恰好是从配置表读的……这种链路一环没卡住,整条就崩。










