pdo::prepare() 和 mysqli_prepare() 真正防注入,因其将sql结构与数据严格分离;而字符串拼接、转义函数等无法区分逻辑与数据,宽字节、数字字段、字符集依赖等问题均使其可被绕过。

必须用 PDO::prepare() 或 mysqli_prepare(),其他写法——包括字符串拼接、mysqli_real_escape_string()、addslashes()——都不算真正防注入。
为什么 mysqli_query() 拼 SQL 一定出事
用户输入直接混进 SQL 字符串里,数据库根本分不清哪是逻辑、哪是数据。比如 $_GET['id'] = "1 OR 1=1",拼成 "SELECT * FROM users WHERE id = 1 OR 1=1",整张表就查出来了。
- 加了单引号也不行:
"id = '" . $_GET['id'] . "'"遇到' OR '1'='1还是崩 -
mysqli_real_escape_string()依赖字符集,宽字节(如 GBK)下可被绕过 - 数字字段不加引号,转义函数压根不生效,
id = "1 OR 1=1"直接执行 -
mysql_*函数 PHP 8.0+ 已彻底移除,连连接都建不了
PDO::prepare() 怎么写才真安全
关键不是调了 prepare(),而是是否禁用了模拟预处理、是否绑定时指定了类型、是否所有外部输入都进了占位符。
- 连接时必须关掉模拟:
PDO::ATTR_EMULATE_PREPARES => false - 错误写法:
$pdo->prepare("SELECT * FROM user WHERE name = '{$_POST['name']}'")—— 这仍是拼接,prepare白调 - 正确绑定方式:用
bindValue()显式传值和类型,比如$stmt->bindValue(1, $_POST['status'], PDO::PARAM_STR) - 命名占位符更清晰:
WHERE status = :status,绑定时用bindValue(':status', $val) -
execute()支持数组传参,但必须确保数组元素和占位符一一对应、类型合理
mysqli_prepare() 绑定参数的硬性规则
bind_param() 不是“把变量塞进去”那么简单,它对变量存在形式、类型标识、引用关系都有强制要求。
- 第一个参数是类型字符串,比如
"si"表示“字符串+整数”,顺序、数量、字母必须和后续变量完全匹配 - 后续变量必须是真实存在的变量(不能是
$_GET['id'] + 1这种表达式),且要传引用(&$email) - 常见报错
Number of variables doesn't match number of parameters就是因为类型字符串长度和变量数对不上 - 整数必须用
i,字符串用s,错一个可能导致值变0或空字符串,且无提示 - 绑定前必须先给变量赋值,不能在
bind_param()调用时现场计算
哪些地方预处理也救不了你
预处理只保「值」的安全,不保「结构」。凡是不能放在 VALUES、WHERE、SET 右侧的位置,都不能用占位符。
-
SELECT * FROM ?—— 报错,表名不可参数化,得用白名单校验:in_array($table, ['users', 'posts']) -
ORDER BY ? DESC—— 报错,字段名和排序方向(ASC/DESC)必须硬编码或白名单映射 -
LIMIT ?虽然语法合法,但用户传负数或超大数会出问题,应先强转:(int)$_GET['limit'] - 动态
GROUP BY、HAVING条件、UNION SELECT的第二部分,全都得走白名单或拒绝机制
最容易被忽略的是:预处理只解决“值怎么进 SQL”,不解决“SQL 本身是不是该这么写”。哪怕用了 bindValue(),如果表名、字段名、排序逻辑全是用户可控又没校验,照样被拖库。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











