sql注入防御核心是禁用字符串拼接,强制使用参数化查询;用户输入须全部视为不可信,表名列名等需白名单校验;禁用mysqli_real_escape_string,pdo须关闭模拟预编译并严格绑定类型。

SQL注入最常发生在字符串拼接场景
只要用户输入直接插进 SQL 语句里,哪怕只有一处没过滤,就可能被绕过。比如用 WHERE name = '$_GET[username]' 这种写法,攻击者传入 ' OR '1'='1 就能逃出单引号上下文。
真正有效的防御不是“审计所有 SQL”,而是从源头切断拼接路径。绝大多数注入漏洞都出在动态拼接上,而不是预编译本身出错。
- 所有用户可控输入(
$_GET、$_POST、$_COOKIE、HTTP 头、文件名、数据库读出再写入的数据)必须视为不可信 - 拼接 SQL 字符串前,先问自己:这个变量是不是由用户控制?如果是,立刻停手,换参数化查询
- 不要依赖“前端校验”或“长度限制”防注入——这些全可绕过
PHP 中必须用 PDO::prepare + bindParam 而非 mysqli_real_escape_string
mysqli_real_escape_string 只对单引号、反斜杠等做转义,但无法覆盖宽字节、多编码上下文、JSON 字段内嵌 SQL 等场景。它本质上是补丁式防御,而现代 PHP 应该用参数绑定。
注意 PDO::prepare 默认不启用真正的预编译(MySQL 模式下是模拟的),所以必须关掉 PDO::ATTR_EMULATE_PREPARES:
pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
- 用
bindParam或bindValue绑定变量,类型要匹配(如整数用PDO::PARAM_INT) - 表名、列名、ORDER BY 字段不能参数化,需白名单校验(例如
in_array($sort, ['created_at', 'status'])) - 避免把参数化当万能解药——如果绑定后又用
eval()或create_function()拼接 SQL,照样崩
ORM 查询也得小心“原生 SQL 插槽”
Laravel 的 whereRaw()、Django 的 extra()、SQLAlchemy 的 text() 都是高危接口。它们看起来像 ORM,实际把字符串直接塞进 SQL 流水线。
比如 Laravel 中这样写就是错的:
->whereRaw("email LIKE '%{$_GET['q']}%'")
正确做法是用占位符:
->whereRaw("email LIKE ?", ["%{$_GET['q']}%"])
- 任何带
Raw、RawQuery、execute_sql字样的方法,都要人工检查是否含用户输入 - ORM 自动处理的
where()、filter()是安全的,但一旦跳出这个封装层,就得自己扛 - 别信文档里“已自动转义”的说法——看源码或测一下
' OR 1=1 --就知道真假
审计不是扫 SQL 字符串,而是找“未绑定的变量插入点”
所谓“对所有 SQL 语句审计”,容易变成机械 grep SELECT、INSERT,结果漏掉 file_get_contents('sql/' . $_GET['tpl'] . '.sql') 这种间接加载。
有效审计应该聚焦三类代码模式:
- 字符串拼接中出现
$开头的变量(尤其是$_系列) - 调用
mysql_query、mysqli_query、PDO::query且第二个参数没用prepare的 - ORM 方法里混用了
Raw、Expression、literal等关键字
真正难防的不是明显拼接,而是多层函数调用后变量“失焦”——比如一个 $id 经过三个函数处理,最后出现在 SQL 里时,已经看不出来源了。











