PHP用mysqli_prepare()防SQL注入的核心是SQL模板与参数分离:占位符?仅用于值,所有外部输入必须经bind_param()绑定,类型严格匹配;动态表名、字段等须白名单校验。

PHP 用 mysqli_prepare() 防 SQL 注入,核心就一条:参数必须走 bind_param()
不绑定参数,prepare() 和拼接字符串没区别。很多人写了 prepare() 却还在 SQL 字符串里直接插变量,比如:"SELECT * FROM user WHERE id = $id" —— 这完全无效,照样被注入。
真正起作用的是「SQL 模板 + 参数分离」:数据库先编译带问号占位符的语句,再把用户数据当纯值传进去,不参与语法解析。
-
prepare()的 SQL 字符串里只能出现?(或命名占位符如:name,但需用PDO) - 所有外部输入(
$_GET、$_POST、文件内容、API 返回值)都必须进bind_param() - 类型标识符
i(int)、s(string)、d(double)、b(blob)要严格匹配,错用会导致静默失败或截断
mysqli vs PDO:选哪个?mysqli_prepare() 的硬伤在哪
mysqli 原生支持预处理,但只支持位置占位符 ?,且 bind_param() 要求所有参数是变量(不能是表达式或数组元素),写起来别扭。例如不能直接写 bind_param('s', $_POST['email']),得先赋值给变量:
$email = $_POST['email'] ?? '';
$stmt = $mysqli->prepare("SELECT id FROM user WHERE email = ?");
$stmt->bind_param('s', $email);而 PDO 支持命名占位符,可直接绑定数组,更灵活:
立即学习“PHP免费学习笔记(深入)”;
$pdo->prepare("SELECT id FROM user WHERE email = :email")->execute(['email' => $_POST['email'] ?? '']);注意:mysqli 的 real_escape_string() 不是预处理,它只是转义,仍有绕过风险(尤其在非 UTF-8 上下文),别把它和 prepare() 混用。
常见翻车现场:这些操作会让预处理失效
预处理防注入的前提是「整条 SQL 结构固定」。一旦动态拼接表名、字段名、ORDER BY 字段或 IN 列表,就回到了拼接字符串的老路。
- 表名/列名不能用
?占位 —— 数据库不允许,会报错mysqli_sql_exception: You have an error in your SQL syntax -
IN (?, ?, ?)的问号个数必须写死,不能根据数组长度动态生成;正确做法是用str_repeat()拼出占位符串,再bind_param()绑定对应数量的变量 - 用
call_user_func_array()绑定不定长参数时,传入的必须是引用数组(&$params),否则bind_param()会报Warning: Parameter 2 to mysqli_stmt::bind_param() expected to be a reference
一个安全又实用的封装示例(mysqli)
避免每次手动 prepare → bind → execute → get_result,可以封装成函数。关键是:只允许占位符用于值,其他动态部分必须白名单校验。
function safe_select($mysqli, $sql, $params = [], $types = '') {
$stmt = $mysqli->prepare($sql);
if (!$stmt) throw new Exception('Prepare failed: ' . $mysqli->error);
if (!empty($params)) {
// 强制转为引用数组
$refs = array();
foreach ($params as $k => $v) $refs[$k] = &$params[$k];
$types = $types ?: str_repeat('s', count($params));
call_user_func_array([$stmt, 'bind_param'], array_merge([$types], $refs));
}
$stmt->execute();
return $stmt->get_result();}
// 使用
$result = safe_select($mysqli, "SELECT name FROM user WHERE status = ? AND level > ?", ['active', 5]);
复杂查询中,表名、排序字段等仍需用白名单过滤(比如 in_array($order, ['created_at', 'name'], true)),这部分没法靠预处理兜底。











