单靠 mysql_real_escape_string 或拼接加引号无法防御现代sql注入;有效防护需分层:参数绑定前置、语义校验居中、运行时监控兜底。

SQL注入防护的深度防线\_采用分层过滤与多维校验
直接说结论:单靠 mysql_real_escape_string 或拼接时加引号,防不住现代SQL注入;真正有效的防护必须分层——参数绑定在最前,语义校验在中间,运行时监控在最后。
为什么预处理语句(Prepared Statement)不能只写一次就完事
很多人以为用了 mysqli_prepare 或 PDO::prepare 就万事大吉,但实际漏洞常出现在“准备”和“执行”之间的逻辑断层里。
- 参数绑定前若已对变量做过字符串拼接(比如
$sql = "SELECT * FROM users WHERE id = ? AND status = '" . $status . "'";),?失效,后半段仍可注入 -
PDO默认不开启PDO::ATTR_EMULATE_PREPARES = false,开启模拟预处理时,MySQL 实际收到的是拼接后的 SQL,?只是占位符语法糖 - 某些 ORM(如旧版 ThinkPHP 3.x)的
where()方法若传入数组且键名可控,底层仍可能触发拼接逻辑
实操建议:检查所有 prepare 调用前的 SQL 字符串是否含任何变量插值;强制设置 PDO::ATTR_EMULATE_PREPARES => false;用 mysqli_stmt_bind_param 时确保类型标记("s"/"i")与实际数据一致,否则可能绕过类型校验。
白名单校验比正则过滤更可靠,但容易被绕过
常见错误是用 preg_replace 过滤 SELECT、UNION 等关键词——攻击者用大小写混写(SeLeCt)、内联注释(SEL/**/ECT)、或编码(%55%4e%49%4f%4e)轻松绕过。
- 字段名、表名、排序方向(
ASC/DESC)这类元数据,必须走严格白名单:in_array($order, ['created_at', 'updated_at'], true) - 动态表名不可来自用户输入,应映射到配置数组:
$table_map = ['user' => 't_user_2024']; $real_table = $table_map[$input] ?? null; - 数字型参数别信
is_numeric(),它接受1e5、+123,而 MySQL 会将其转为浮点数参与查询,可能引发精度偏差或隐式类型转换漏洞
性能影响小,但兼容性要注意:部分老系统依赖 mysql_query + 手动转义,迁移到白名单需同步清理所有 mysql_* 函数调用,否则混合使用反而扩大攻击面。
WAF 和数据库审计日志不是摆设,但得配对看
单独看 WAF 日志里的 UNION SELECT 告警没意义——很多合法报表导出功能本身就用 UNION;单独看数据库慢查日志也看不出异常,因为注入载荷可能藏在毫秒级查询里。
- 把 WAF 的
client_ip+request_uri和数据库审计日志里的user_host+query_time对齐,才能确认某次“看似正常”的请求是否触发了非常规子查询 - 重点监控
information_schema表访问、空条件查询(WHERE 1=1)、大量sleep()或benchmark()调用——这些在业务逻辑里几乎不会出现 - MySQL 8.0+ 的
audit_log插件默认不记录参数值,需开启audit_log_include_accounts并配合应用层日志做参数还原
最常被忽略的是时间差:攻击者用 IF(1=1, SLEEP(5), 0) 探测逻辑,这种请求在数据库日志里就是一条普通 SELECT,但响应时间突增,必须和 Nginx access log 的 $request_time 联动分析。










