唯一可靠的方式是使用预处理语句配合参数化查询,因其将SQL结构与数据完全分离,从根本上杜绝语法混淆;其他如mysql_real_escape_string、addslashes等仅转义字符,无法覆盖所有上下文且存在宽字节漏洞。

PHP 中防止 SQL 注入,**唯一可靠的方式是使用预处理语句(Prepared Statements)配合参数化查询**,而不是依赖过滤函数、字符串替换或正则“清洗”。
为什么 mysql_real_escape_string 和 addslashes 不够安全
这些函数只是对引号等字符做转义,但无法覆盖所有上下文(比如数字型参数、ORDER BY 子句、表名/字段名),且在多字节编码(如 GBK)下存在宽字节注入漏洞。一旦字符集设置不一致或拼接逻辑稍有偏差,防线就失效。
-
addslashes完全不检查当前连接的字符集,对%df%27这类宽字节 payload 无防护能力 -
mysql_real_escape_string已被废弃(PHP 7.0+ 移除),且只适用于mysql_*扩展(早已弃用) - 任何基于“过滤输入”的思路(如
strip_tags、htmlspecialchars)都错位——它们解决的是 XSS,不是 SQL 注入
必须用 PDO::prepare 或 mysqli_prepare
预处理语句将 SQL 结构和数据完全分离:数据库先编译语句模板,再把参数作为纯数据绑定进去,从根本上杜绝语法混淆。
正确示例(PDO):
立即学习“PHP免费学习笔记(深入)”;
$pdo = new PDO($dsn, $user, $pass);
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = ? AND status = ?");
$stmt->execute([$id, $status]);
$user = $stmt->fetch();命名参数更清晰(尤其多参数时):
$stmt = $pdo->prepare("SELECT * FROM posts WHERE author_id = :uid AND published = :flag");
$stmt->execute(['uid' => $_GET['uid'], 'flag' => 1]);- 占位符只能用于**数据值**,不能用于表名、字段名、排序方向(
ORDER BY ?是语法错误) - 若真需动态列名,必须白名单校验:
in_array($sort_field, ['created_at', 'title'], true) - 确保
PDO::ATTR_EMULATE_PREPARES设为false(默认 MySQL 驱动已关闭模拟预处理)
别碰 $_GET、$_POST 等原始输入直接拼 SQL
哪怕你写了 intval() 或 (int) 强转,只要后续还混进字符串拼接,风险就仍在。例如:
// ❌ 危险!即使 intval 了,拼接后仍是字符串上下文 $id = intval($_GET['id']); $sql = "SELECT * FROM comments WHERE post_id = " . $id . " AND deleted = 0"; // 假设 $id 被绕过或类型误判
更隐蔽的问题是类型松散比较导致的逻辑绕过(如 "0abc" == 0),所以不要试图“加固拼接”,而要彻底避免拼接。
- 所有用户输入进入 SQL 前,必须走预处理绑定
- 对非数值场景(如搜索关键词),也必须用
?或:param,不要用sprintf或双引号插值 - ORM(如 Laravel Eloquent、Doctrine)默认使用预处理,但需确认没调用
whereRaw或DB::raw暴露原始 SQL
真正难的不是写对那几行 prepare,而是把“参数化”变成肌肉记忆——只要 SQL 字符串里出现任何 .$var. 或 "{$var}",就得立刻停下来重写。











