预处理语句防SQL注入必须满足三要素:所有外部输入进占位符(?或:name)、禁用模拟预处理(PDO::ATTR_EMULATE_PREPARES=false)、连接时显式指定字符集(如charset=utf8mb4);否则仍存在注入风险。

预处理语句必须用对,不是调用了prepare就安全
很多人以为只要写了 $pdo->prepare() 就万事大吉,其实关键在“怎么写 SQL 模板”和“怎么传参数”。如果模板里还混着变量拼接,比如 "SELECT * FROM user WHERE name = '{$_POST['name']}'",那 prepare() 完全没起作用——它只是把这串已拼好的字符串当模板编译了。
- 所有用户输入必须进占位符:
?或:name,不能出现在引号内、不能拼在 SQL 字符串里 - 数字型字段(如
id = ?)也必须用占位符,mysql_real_escape_string对它完全无效 -
execute()必须传数组,不能传字符串;bindValue()要显式指定类型(如PDO::PARAM_INT),避免自动转换出错 - PDO 默认开启模拟预处理(
PDO::ATTR_EMULATE_PREPARES = true),建议连接时强制关掉:['PDO::ATTR_EMULATE_PREPARES' => false]
表名、列名、ORDER BY 这些不能用占位符,得靠白名单
预处理只保护“值”,不保护 SQL 结构本身。你没法写 "SELECT * FROM ?" 或 "ORDER BY ?"——数据库会直接报错。这类动态片段必须提前限定范围,运行时只做匹配,不拼接。
- 排序字段:用
in_array($_GET['sort'], ['id', 'created_at', 'status']) ?: 'id' - 搜索字段:若支持按 name/email 搜索,先定义允许字段数组,再取键:
$field = array_key_exists($_GET['field'], ['name'=>'name','email'=>'email']) ? $_GET['field'] : 'name'; - IN 查询:不能直接传数组进字符串,要动态生成等量
?占位符:$placeholders = str_repeat('?,', count($ids) - 1) . '?'; $sql = "WHERE id IN ($placeholders)";,再绑定$ids数组
字符集没设对,宽字节注入就能绕过预处理
即使用了预处理,如果连接层字符集不一致,攻击者仍可能用 %df%27 这类宽字节编码触发截断,让转义失效。这不是预处理的锅,是连接配置漏了。
- PDO 连接 DSN 中必须显式声明字符集:
"mysql:host=localhost;dbname=test;charset=utf8mb4" - MySQLi 要在连接后立刻调用:
$mysqli->set_charset('utf8mb4'),不能只靠配置文件 - 避免用旧版
utf8(实际是 utf8mb3),它不支持 emoji 和部分生僻字,且宽字节处理有历史缺陷
别信 filter_var 或 intval 就能代替预处理
有人对 ID 做 intval($_GET['id']),觉得“已经是整数了,拼进去也安全”,这是危险错觉。整数上下文虽不加引号,但若后续逻辑扩展成 "id = " . $id . " AND status = {$_GET['status']}",status 一松动就全崩。验证和预处理不是二选一,是两道独立防线。
-
filter_var()用于校验格式(邮箱、URL),不是防注入的替代方案 -
intval()可作兜底,但不能省略预处理;它对空字符串、负数、超限值的行为需额外判断 - 真正该拼的只有白名单里的静态字符串(如固定表名、枚举值),其他一切外部输入都走占位符
预处理不是魔法开关,它只在“SQL 模板干净 + 参数绑定到位 + 字符集统一”的前提下才真正生效。最容易被忽略的是模拟预处理开关和宽字节字符集设置——这两个点一漏,前面所有努力都可能白费。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











