预处理语句是唯一能从根源切断sql注入的手段,因其将sql结构与参数值分两阶段处理,确保恶意输入仅作字面量代入;其他过滤、转义等均为补丁,无法替代。

预处理语句不是“可选优化”,而是唯一能从根源切断SQL注入的手段;其他所有措施——过滤、转义、关键词黑名单——都只是补丁,无法替代。
为什么prepare() + execute() 是不可绕过的底线
数据库在执行预处理语句时,会把SQL结构(如SELECT * FROM users WHERE id = ?)和参数值(如123)分两阶段处理:先编译语法树,再代入数据。这意味着即使你传入"123 OR 1=1",它也只会被当作一个字符串字面量,绝不会参与逻辑判断。
-
prepare()必须在绑定前调用,且不能对已拼接的SQL字符串调用——比如"SELECT * FROM users WHERE name = '" . $_GET['name'] . "'"再套prepare()毫无意义 - PDO默认启用模拟预处理(
PDO::ATTR_EMULATE_PREPARES = true),这会让PHP自己做参数替换,失去数据库层防护;务必显式关闭:$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false) - MySQLi面向对象方式中,
bind_param()的第一个参数是类型字符串,"s"(string)、"i"(integer)、"d"(double)、"b"(blob)必须与实际变量类型严格匹配,否则绑定失败或值被截断
filter_var() 和正则白名单只能防误操作,不能防绕过
输入验证是前置守门员,但它的职责是拒绝明显非法输入(比如邮箱里塞admin'--),而不是替预处理兜底。一旦攻击者构造出符合格式的恶意输入(例如用十六进制编码绕过filter_var($email, FILTER_VALIDATE_EMAIL)),验证就失效了。
- 数字型参数别只用
is_numeric()——它会放行"123e4"这种科学计数法字符串;优先用filter_var($input, FILTER_VALIDATE_INT)或强制类型转换(int)$input - 用户名等标识符必须用白名单正则,且锚定首尾:
preg_match('/^[a-zA-Z0-9_]{3,16}$/', $username),漏掉^或$会导致"admin\0anything"类绕过 - 禁止对已通过验证的数据再做
addslashes()或mysql_escape_string()——这些函数不依赖连接上下文,转义结果不可信;仅在遗留代码中且无法改用预处理时,才用mysqli_real_escape_string($conn, $str)
命名参数 vs 问号占位符:选哪个更安全?
两者安全性无差别,区别在于可维护性。命名参数(如:username)在复杂查询中更容易追踪绑定关系,而问号(?)要求参数顺序与占位符位置严格一致,错一位就全乱。
- PDO支持两种写法,但命名参数必须配合关联数组:
$stmt->execute([':username' => $user, ':status' => $status]);若混用索引数组会静默失败 - MySQLi只支持问号占位符,且
bind_param()必须传变量引用(bind_param("ss", $user, $pass)),不能传表达式或函数调用结果 - ORM如Eloquent或Doctrine底层仍走预处理,但要注意某些动态构建查询的方法(如
whereRaw())会跳过参数化,需人工确保内部不拼接用户输入
真正容易被忽略的,是连接初始化阶段的配置——PDO默认错误模式是静默失败,prepare()出错时只返回false而不抛异常,开发者若没检查返回值,就会误以为预处理成功了;务必设为异常模式:$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION)。











