mysqli::prepare() + bind_param() 能防SQL注入,因其在服务端先编译SQL模板、再将参数作为独立数据传入,严格区分指令与值;而 mysqli_real_escape_string() 等过滤方式易被宽字节、十六进制等绕过,且不适用于表名、ORDER BY 等动态位置。

必须用 mysqli::prepare() + bind_param(),不能只调用 mysqli_real_escape_string() 或拼接字符串——否则仍会被宽字节、十六进制编码或注释符绕过。
为什么 mysqli_prepare() 能防注入
预处理语句在 MySQL 服务端先编译 SQL 模板(如 "SELECT * FROM users WHERE id = ?"),再把参数作为独立数据传入执行。数据库引擎明确区分“指令”和“值”,即使用户输入 "1 OR 1=1 -- ",它也只会被当作文本值,不会参与语法解析。
常见错误是误以为 mysqli_real_escape_string() 就够了——它只对单引号、反斜杠转义,且依赖连接字符集;一旦用在 ORDER BY、表名、数字字段或宽字节上下文,立刻失效。
- 必须确保连接字符集设为
utf8mb4:$mysqli->set_charset('utf8mb4'),否则宽字节注入可能成功 - 占位符
?不能出现在 SQL 关键字位置(如ORDER BY ?),这类动态片段必须用白名单校验 - 不支持多语句执行(如
"SELECT ...; DROP TABLE ..."),mysqli::prepare()会直接拒绝
绑定参数时类型标识必须准确
bind_param() 第一个参数是类型字符串,每个字符对应后续一个变量,顺序、个数、类型都错不得。类型不匹配不会报错,但可能导致绑定失败、查不到数据或隐式转换漏洞。
立即学习“PHP免费学习笔记(深入)”;
-
"s":字符串(含空值) -
"i":有符号整数((int)强转后传入) -
"d":双精度浮点数 -
"b":BLOB,需配合mysqli_stmt_send_long_data()
示例:
$stmt = $mysqli->prepare("SELECT * FROM products WHERE category_id = ? AND price < ?");
$stmt->bind_param("id", $cat_id, $max_price);
$cat_id = (int)$_GET['cat'] ?? 1;
$max_price = (float)$_GET['max'] ?? 999.99;
$stmt->execute();常见翻车点:变量作用域与复用语句对象
bind_param() 绑定的是变量的引用,不是当前值。如果在循环中复用同一个 $stmt,必须每次重新赋值变量,否则会绑定旧值。
- 错误写法:在
for循环里只调一次bind_param(),然后反复execute() - 正确做法:每次循环前更新变量值,
execute()自动使用最新绑定值(无需重绑) - 注意
bind_param()的第一个参数是类型串,后面必须是变量名(不能是表达式或字面量),所以$stmt->bind_param("s", $_GET['name'])是非法的
安全写法:
$stmt = $mysqli->prepare("INSERT INTO logs (ip, action) VALUES (?, ?)");
$stmt->bind_param("ss", $ip, $action);
<p>foreach ($requests as $req) {
$ip = $req['ip'] ?? '0.0.0.0';
$action = $req['action'] ?? 'unknown';
$stmt->execute();
}别忘了错误检查和连接初始化
PHP 8.1 中 mysqli 默认不抛异常,prepare() 失败返回 false,但容易被忽略。建议开启异常模式或手动检查。
- 初始化时启用异常:
$mysqli->options(MYSQLI_OPT_CONNECT_TIMEOUT, 5); $mysqli->options(MYSQLI_OPT_READ_TIMEOUT, 10); - 检查
prepare()返回值:if (!$stmt = $mysqli->prepare($sql)) die("Prepare failed: " . $mysqli->error); -
execute()后可用$stmt->affected_rows判断是否真有写入,避免静默失败
最关键的一点:所有用户输入,只要进 SQL,就必须走 prepare() + bind_param() 这条路径。没有例外,也没有“这个字段看起来安全”的侥幸空间。



















