mysql_query()拼接SQL最危险,因用户输入直接嵌入语句导致SQL注入;预处理通过参数与结构分离防御,但表名、字段名等动态结构仍需白名单校验。

为什么 mysql_query() 拼接 SQL 是最危险的写法
因为字符串拼接会把用户输入直接塞进 SQL 语句里,数据库根本分不清哪部分是逻辑、哪部分是数据。哪怕加了 addslashes() 或 htmlspecialchars(),也防不住绕过、编码混淆、多字节截断等攻击路径。
- 真实场景:登录时用
$_POST['username']和$_POST['password']拼出"SELECT * FROM users WHERE name = '$u' AND pass = '$p'"—— 攻击者输' OR '1'='1就能绕过验证 -
mysql_*函数在 PHP 7.0+ 已被移除,继续用等于主动放弃安全更新和维护支持 - 预处理不是“加个函数就行”,核心在于「参数与 SQL 结构分离」—— 数据永远不参与 SQL 解析阶段
用 mysqli_prepare() 写安全查询的最小可行步骤
不用框架、不依赖 ORM,纯原生 mysqli 预处理,三步闭环:准备 → 绑定 → 执行。
- 先调用
mysqli_prepare($conn, "SELECT id FROM users WHERE email = ? AND status = ?"),问号占位,SQL 结构固定 - 再用
mysqli_stmt_bind_param($stmt, "si", $email, $status)绑定变量,"si"表示第一个参数是 string、第二个是 integer - 最后
mysqli_stmt_execute($stmt),数据库只把绑定值当纯数据处理,不会重解析语法 - 注意:
bind_param()的第一个参数必须是引用(&$email),传值会报Warning: Parameter must be passed by reference
PDO::prepare() 和 bindValue() 的实际差异点
PDO 更灵活,但容易因绑定方式选错导致失效。关键区别不在语法,而在「值何时被确定」。
-
bindValue($param, $value, PDO::PARAM_STR):立即把当前$value的值拷贝进去,后续改$value不影响已绑定内容 -
bindParam($param, $value, PDO::PARAM_STR):绑定的是变量本身(类似引用),执行前最后一次读取$value的值 —— 循环中反复执行同一条语句时有用 - 常见坑:
bindValue(':id', $_GET['id'])如果没校验$_GET['id']类型,整数 ID 被传成字符串,可能触发隐式类型转换,让索引失效 - 建议统一用
bindValue(),显式控制类型,避免意外引用行为
预处理不能自动防住的所有情况
预处理只保「值」的安全,不保「结构」。表名、字段名、排序方向、LIMIT 数量这些动态部分,依然得靠白名单或强校验。
立即学习“PHP免费学习笔记(深入)”;
- 错误写法:
"SELECT * FROM ? ORDER BY ? DESC"——prepare()不支持对表名/字段名占位,会直接报错SQLSTATE[HY000]: General error - 正确做法:用白名单映射,比如
$allowed_tables = ['users', 'posts']; if (!in_array($table, $allowed_tables)) die('Invalid table'); - LIMIT 后的数字必须转整型:
$limit = (int)$_GET['limit']; $stmt->bindValue(':limit', $limit, PDO::PARAM_INT);,否则可能被注入10 UNION SELECT ... - 像
LIKE查询中的通配符(%)要由业务代码控制,不要让用户直接输%,更不要拼进占位符前后的字符串里
真正难的从来不是写对那几行 prepare 和 bind,而是想清楚哪些地方「看起来像数据,其实是结构」—— 这类地方,预处理帮不上忙,只能靠设计约束和校验兜底。











