必须使用 PDO::prepare() 或 mysqli::prepare() 配合参数绑定,禁用模拟预处理、严格匹配数据类型、设置正确字符集,其他过滤转义手段均不可靠。

直接用 PDO::prepare() 或 mysqli::prepare(),其他所有“过滤”“转义”“替换”手段都不可靠,不能替代预处理。
为什么 mysqli_real_escape_string() 不够用
它只是对单引号、反斜杠等做简单转义,但面对宽字节注入(如 %df%27)、十六进制编码(0x27)、或 MySQL 注释符组合(' -- 、' #)时完全失效。更关键的是:它仍把变量拼进 SQL 字符串里,数据库在解析阶段无法区分哪部分是结构、哪部分是数据。
常见错误写法:
$sql = "SELECT * FROM users WHERE name = '" . mysqli_real_escape_string($conn, $_GET['name']) . "'";
这种写法看起来“转义了”,但只要连接字符集没设对(比如没调用 mysqli_set_charset($conn, 'utf8mb4')),攻击者就能绕过。
立即学习“PHP免费学习笔记(深入)”;
PDO::prepare() 必须关掉模拟预处理
PDO 默认开启 PDO::ATTR_EMULATE_PREPARES = true,这意味着 PHP 自己在客户端“假装”做了参数绑定,实际发给 MySQL 的仍是拼接后的 SQL——等于白防。
正确初始化方式:
$pdo = new PDO($dsn, $user, $pass, [
PDO::ATTR_EMULATE_PREPARES => false,
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
]);验证是否生效:
- 检查
$pdo->getAttribute(PDO::ATTR_CLIENT_VERSION)是不是真实 MySQL 客户端版本(不是“emulated”) - MySQL 服务端版本需 ≥ 5.1.17 才支持完整原生预处理
- 执行
SHOW VARIABLES LIKE 'have_prepared_statement'确认服务端已启用
mysqli::prepare() 绑定参数时类型要严格匹配
类型标识符错一个,就可能触发隐式转换,让绑定失效或产生非预期行为。比如整数字段传了 "s"(字符串),MySQL 可能自动转成数字再比较,但某些边界值(如超大整数)会截断或溢出。
必须按实际数据类型选:
-
"s":字符串(含空值、NULL) -
"i":有符号整数(不要用(int)强转后传"s") -
"d":浮点数(别用字符串格式的"1.23"配"s") -
"b":BLOB,需配合send_long_data()
示例:
$stmt = $mysqli->prepare("SELECT * FROM logs WHERE user_id = ? AND level = ?");
$stmt->bind_param("is", $uid, $level); // $uid 是 int,$level 是字符串
$uid = (int)$_GET['uid']; // 类型转换在这里做,不是 bind_param 里
$level = $_GET['level'] ?? 'info';
$stmt->execute();别碰 mysql_* 函数,也别信“正则过滤”
mysql_connect()、mysql_query() 在 PHP 7.0+ 已彻底移除,代码运行直接报 Fatal error。任何还在用它们的项目,不光有注入风险,连启动都成问题。
用 str_replace() 或正则删掉 '、;、-- 之类,本质是黑名单思维——漏一个变种就崩。比如攻击者用 /* */ 注释、用 UNION/**/SELECT 绕过空格检测、甚至用双写 SELSELECTECT 混淆 WAF。
真正该做的只有两件:
① 所有数据库交互走 prepare() + bind_param() 或 prepare() + execute();
② 连接初始化时明确设置字符集(utf8mb4)和错误模式(异常抛出)。
最易被忽略的一点:预处理只保“查询语句”,不保表名、字段名、ORDER BY 子句。如果业务真要动态列名,只能用白名单硬编码映射,绝不能拿用户输入直接拼。



















