mysqli_prepare()不能拼接表名列名因占位符仅保护值位置,表名等须白名单校验;绑定参数只防数据注入,权限与日志盲区仍需业务层控制。

为什么 mysqli_prepare() 不能直接拼接 SQL 字符串
因为预处理语句的「参数占位符」只允许出现在值的位置,不能替代表名、列名或关键字。一旦你写成 "DELETE FROM $table WHERE id = ?",$table 是 PHP 变量拼进去的,攻击者仍可控制它——比如传入 "users; DROP TABLE users; -- ",就完成注入。
- 只有
?或:name占位符才受预处理机制保护,且仅限于数据值 - 表名、字段名、ORDER BY 子句、LIMIT 数值(非 MySQLi 的
bind_param范围)必须提前白名单校验或硬编码 - MySQLi 中
mysqli_prepare()对非法 SQL 结构不报错,但执行时可能失败,掩盖注入风险
PHP 中用 mysqli_stmt::bind_param() 绑定 DELETE 参数的正确姿势
绑定只解决「值」的安全问题。比如删除用户时根据 ID 或邮箱操作,这些才是该走预处理的地方。
- SQL 模板固定:
"DELETE FROM users WHERE id = ?"或"DELETE FROM users WHERE email = ?" - 调用
bind_param()时类型标记要匹配:ID 用"i"(整型),邮箱用"s"(字符串) - 不要试图用
"s"绑定整个条件表达式,如"id = ? OR status = ?"是合法的,但"? = ?"会出错——占位符不能在操作符位置
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
$stmt = $mysqli->prepare("DELETE FROM users WHERE id = ?");
$stmt->bind_param("i", $user_id);
$stmt->execute();
PostgreSQL 和 SQLite 的等效做法差异
不是所有数据库的预处理语法都一样。PHP 的 PDO 更统一,但底层驱动行为仍有区别。
- PDO + PostgreSQL:用
:id命名占位符更清晰,bindValue(':id', $user_id, PDO::PARAM_INT)显式指定类型 - SQLite3 类不支持 bindParam,得用
SQLite3Stmt::bindValue(),且只接受SQLITE3_INTEGER/SQLITE3_TEXT等常量,不能传 PHP 类型字符串 - MySQLi 不支持命名参数(除非用 PDO 封装),强行用
:id会被当字面量,导致无匹配行却静默成功
DELETE 操作里最容易被忽略的权限与日志盲区
预处理防不了越权,也留不下完整上下文。一个合法的 id = ? 绑定,可能删掉别人的数据。
- 业务层必须校验当前用户是否有权操作该记录,例如
WHERE id = ? AND owner_id = ?,两个参数都绑定 - 别依赖数据库日志查“谁删了什么”——MySQL 的 general_log 不记录绑定后的实际值,binlog 默认也不含原始参数
- DELETE 无 WHERE 子句是高危操作,即使用了预处理;上线前应禁用或加双重确认(如配置项
safe-updates)
真正麻烦的从来不是怎么写那行 bind_param,而是删之前没确认身份、没留审计痕迹、没限制作用域。










