使用PDO或mysqli预处理并绑定参数是PHP防SQL注入唯一可靠方法,须禁用模拟预处理、开启异常模式,注意mysqli类型与引用要求,标识符需白名单校验,输入验证不可替代。

用 PDO::prepare() 或 mysqli::prepare() 配合参数绑定,是当前 PHP 防 SQL 注入唯一可靠路径。其他方法(如 addslashes()、mysql_real_escape_string())已废弃或存在绕过风险,不能作为主力防护手段。
必须禁用 PDO 模拟预处理
默认情况下,PDO 可能启用 PDO::ATTR_EMULATE_PREPARES => true,此时 prepare 实际只是客户端字符串拼接,完全不经过 MySQL 服务端预编译,形同虚设。
- 务必在创建 PDO 实例时显式关闭:
[PDO::ATTR_EMULATE_PREPARES => false] - 同时开启异常模式:
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,避免错误静默失败 - 若连接后执行
$pdo->getAttribute(PDO::ATTR_EMULATE_PREPARES)返回true,说明配置未生效 - 某些旧版 MySQL(如 5.5 且未设
sql_mode=STRICT_TRANS_TABLES)可能触发退化,需实测验证
mysqli 中 bind_param() 的类型与引用陷阱
mysqli_stmt::bind_param() 要求变量必须是引用,且类型字母必须严格匹配 —— 这是线上最常出错的两个点。
- 类型码只能是
i(int)、d(double)、s(string)、b(blob),传n或str会直接报错 - 所有变量参数必须加
&引用符号,例如:$stmt->bind_param('ss', $ip, $action)错,应为$stmt->bind_param('ss', $ip, $action)(注意:PHP 7.4+ 允许非引用,但低版本仍需显式&$ip) - 绑定前不要对变量调用
mysqli_real_escape_string(),否则双重处理可能导致截断或乱码 - 数字型输入建议先转整型:
$id = (int)$_GET['id'];,再传入bind_param('i', $id),避免字符串隐式转换漏洞
表名、字段名、ORDER BY 不能参数化,必须白名单校验
占位符 ? 和 :name 只能用于数据值,不能用于标识符(identifier)。强行拼接会导致注入,且无任何转义函数可安全处理。
立即学习“PHP免费学习笔记(深入)”;
- 动态排序字段必须白名单过滤:
in_array($_GET['sort'], ['id', 'name', 'created_at']) ?: 'id' - 动态表名建议映射到固定键:
$tables = ['user' => 'users', 'log' => 'admin_logs']; $table = $tables[$_GET['type']] ?? 'users'; - 禁止用
filter_var($input, FILTER_SANITIZE_STRING)处理字段名 —— 它不校验语义,只删标签,毫无防护作用 - 若业务真需高度动态结构(如低代码平台),应走元数据查询 + 权限检查 + 语法树解析,而非字符串拼接
预处理不是万能解药,输入验证仍是必要防线
即使用了 prepare(),错误的输入仍可能引发逻辑漏洞、数据污染或拒绝服务(如超长字符串撑爆索引、负数绕过价格校验)。
- 数字参数优先用
filter_var($input, FILTER_VALIDATE_INT, ['options' => ['min_range' => 1, 'max_range' => 9999]]) - 邮箱、URL、IP 等结构化字段必须用对应
FILTER_VALIDATE_*校验,不能只靠绑定 - 自由文本长度应在应用层硬限制(如
mb_strlen($bio) ),数据库 <code>VARCHAR(200)不是保险栓 - 绑定参数后仍要检查
$stmt->rowCount()或返回值,防止“查不到却没报错”类盲注辅助条件
最容易被忽略的是:预处理只保 SQL 结构安全,不保业务逻辑安全。一个被正确绑定的 WHERE id = ?,如果传入了越权 ID(如管理员 ID),照样导致水平越权 —— 这时候需要的是权限校验,不是防注入。



















