唯一可靠防sql注入方式是预处理语句(pdo::prepare或mysqli_prepare),因mysql_real_escape_string依赖字符集且无法防护数字参数、字段名等非值上下文,而预处理仅保护sql值,表名、排序字段等须白名单校验。

PHP 防 SQL 注入,唯一可靠方式是用预处理语句(PDO::prepare 或 mysqli_prepare),拼接字符串、过滤函数(如 mysql_real_escape_string)或正则替换都不够安全。
为什么 mysqli_real_escape_string 不足以防注入
它只对引号和反斜杠做转义,依赖字符集设置;若数据库连接未显式指定编码(如没调用 mysqli_set_charset($conn, 'utf8mb4')),攻击者可用宽字节绕过。更关键的是:它无法防护数字型参数、ORDER BY 字段名、表名等非值上下文。
常见错误现象:mysqli_query($conn, "SELECT * FROM user WHERE id = " . $_GET['id']) 即使加了 mysqli_real_escape_string,传入 id=1 OR 1=1 仍会全表返回——因为数字字段不加引号,根本不会触发转义逻辑。
- 数字参数必须用占位符,不能拼进 SQL 字符串
- 字段名、表名、排序方向(
ASC/DESC)无法用预处理绑定,需白名单校验 -
mysql_*函数已废弃,PHP 8.0+ 完全移除,别再用
PDO::prepare 的正确写法与坑点
预处理不是“用了就安全”,关键在绑定方式和执行时机。错误示例:$stmt = $pdo->prepare("SELECT * FROM user WHERE name = '{$_POST['name']}'"); —— 这仍是字符串拼接,prepare 没起作用。
正确做法是所有外部输入都用 ? 或命名占位符,再通过 bindValue 或 bindParam 绑定:
$stmt = $pdo->prepare("SELECT * FROM user WHERE status = ? AND created_at > ?");
$stmt->bindValue(1, $_POST['status'], PDO::PARAM_STR);
$stmt->bindValue(2, $_POST['since'], PDO::PARAM_STR);
$stmt->execute();
- 不要用
exec()执行含占位符的语句,它不支持绑定 -
PDO::ATTR_EMULATE_PREPARES默认开启(尤其 MySQL),此时 PDO 自己模拟预处理,可能绕过服务端预编译,建议设为false强制走真实预处理 - 绑定时指定类型(
PDO::PARAM_INT/PDO::PARAM_STR),避免自动类型转换引发意外行为
mysqli_prepare 和 bind_param 的参数陷阱
mysqli_prepare 要求 bind_param 的第一个参数是类型字符串,每个字符对应后续变量类型,顺序和数量必须严格匹配。错一个就报 Number of variables doesn't match number of parameters in prepared statement。
示例:
$stmt = $mysqli->prepare("INSERT INTO log (ip, action, code) VALUES (?, ?, ?)");
$stmt->bind_param("ssi", $_SERVER['REMOTE_ADDR'], $_POST['action'], $_POST['code']);
$stmt->execute();
- 类型字符串
"ssi"中:s表示 string,i表示 integer;d是 double,b是 blob - 变量必须是引用(
bind_param内部按引用传递),所以不能直接写bind_param("s", "hello"),要先赋值给变量 - 如果参数是数组或对象,需先解构;
bind_param不支持可变参数展开(PHP 8.1+ 的...也不行)
哪些地方预处理也救不了你
预处理只保护「SQL 值」,对动态表名、列名、GROUP BY 字段、LIMIT 偏移量等无效。这些必须用白名单硬控制,不能靠任何“过滤”或“转义”。
比如用户选排序字段:$_GET['sort'] = 'name DESC';,不能直接拼进 SQL。应限定为:
$allowed_sorts = ['name' => 'name', 'created_at' => 'created_at', 'score' => 'score']; $sort = $allowed_sorts[$_GET['sort']] ?? 'id'; $sql = "SELECT * FROM user ORDER BY $sort DESC";
-
LIMIT的偏移量和数量必须是整数,用(int)强转,再检查是否非负、是否超上限 - 表名、数据库名若需动态,只能从配置数组中取,禁止任何用户输入参与拼接
- ORM(如 Laravel Eloquent)底层也用预处理,但若混用原生查询(
DB::raw())或whereRaw,仍可能引入漏洞
真正难的不是写对一条 prepare,而是整个应用里每处数据库交互都保持绑定习惯,并且对非值上下文有清晰的白名单意识——漏掉一个 $_GET 参数、一次手快的字符串拼接,就前功尽弃。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











