唯一能从执行机制上切断sql注入路径的方式是pdo::prepare()或mysqli::prepare()配合参数绑定;其他过滤、转义均为无效补丁,必须严格遵循prepare→bind_param/execute→执行链路,且字符集须设为utf8mb4、占位符仅用于值而非标识符。

直接用 PDO::prepare() 或 mysqli::prepare(),配合参数绑定,是唯一能从执行机制上切断SQL注入路径的方式。其他过滤、转义、替换全是补丁,对现代绕过手法基本无效。
必须用 prepare() + bind_param() 或 execute(),不能只调用 prepare() 就完事
常见错误是写了 $stmt = $mysqli->prepare($sql),但后面直接拼接变量进 SQL 字符串,或者用 mysqli_query() 执行——这等于没用预处理。
正确链路只有一条:prepare 定义模板 → bind_param / execute 传值 → 执行。中间任何一步跳过或替换成字符串拼接,防御就失效。
-
bind_param()的类型标识必须准确:"s"(字符串)、"i"(整数)、"d"(浮点)、"b"(BLOB),类型错可能触发隐式转换,导致绕过 -
execute()传数组时,顺序/键名必须和占位符严格对应;问号占位符用索引数组,命名占位符用关联数组 - 不要在占位符位置塞表名、字段名、
ORDER BY子句——这些必须走白名单校验后硬编码
字符集不设对,预处理也会被宽字节注入绕过
MySQL 连接默认字符集可能是 latin1 或旧版 utf8,而 PHP 接收的输入往往是 UTF-8。当存在多字节字符(如中文、emoji)时,mysqli_real_escape_string() 类函数会失准,预处理语句的底层解析也可能出错。
解决方法只有一个:连接建立后立刻设置完整 UTF-8 支持。
- PDO 连接时在 DSN 中加
charset=utf8mb4:"mysql:host=localhost;dbname=test;charset=utf8mb4" - MySQLi 要显式调用
$mysqli->set_charset('utf8mb4'),不能只靠mysqli_set_charset()全局函数 - 确认数据库、表、字段实际字符集也是
utf8mb4,否则连接层设对了也没用
LIKE 查询里的通配符必须在 PHP 层加,不能拼进 SQL 模板
写 WHERE name LIKE '%?%' 是错的——问号占位符会被当成字面量,最终查的是带百分号的用户名,不是模糊匹配。
正确做法是把通配符加在 PHP 变量里,再绑定:
$keyword = $_POST['q'] ?? '';
$search = "%{$keyword}%";
$stmt = $pdo->prepare("SELECT * FROM items WHERE title LIKE ?");
$stmt->execute([$search]);
- 如果要用命名占位符,也一样:
title LIKE :search,然后execute(['search' => $search]) - 注意对
$keyword做基础过滤(比如trim()、长度限制),防止超长或空值引发性能问题 - 别试图在 SQL 里用
CONCAT('%', ?, '%')——部分 MySQL 版本或驱动对函数内占位符支持不稳定
旧代码改造时最容易漏掉的三个点
很多项目不是从零开始,而是边维护边加固。以下三处高频遗漏,一碰就崩:
- 所有
mysql_query()、mysqli_query()(非对象风格)、pg_query()等裸查询调用,必须替换成预处理;grep 搜索query(和_query能快速定位 - 动态排序字段(如
ORDER BY $_GET['sort'])不能绑参,必须用白名单:$allowed = ['id', 'name', 'created_at']; $sort = in_array($_GET['sort'], $allowed) ? $_GET['sort'] : 'id'; - 分页
LIMIT ?, ?要验证两个参数都是整数,且offset≥ 0、limit≤ 100,否则恶意请求可能拖垮数据库
预处理本身不难,难的是所有分支路径都走同一套安全链路。一个 $_GET 参数没校验、一处 ORDER BY 没白名单、一次连接没设字符集,整个防护就形同虚设。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











