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