唯一防SQL注入的方法是参数化查询,因字符串拼接无论加何种转义均可能被编码绕过;需分离SQL结构与用户数据,PDO更推荐且应禁用模拟预处理,动态标识符须用白名单校验。

用 mysqli::prepare() 或 PDO::prepare() 是唯一可靠方式
手写字符串拼接 SQL(哪怕加了 addslashes() 或 mysql_real_escape_string())本质上无法覆盖所有编码绕过场景,PHP 官方早已废弃 mysql_* 系列函数。真正防注入只有一条路:参数化查询。
关键不是“过滤输入”,而是“分离代码与数据”——SQL 语句结构由开发者固定,用户输入只作为参数传入,数据库驱动层自动处理转义和类型绑定。
-
PDO更推荐:支持多种数据库、默认强制模拟预处理(可关)、错误模式易调试 -
mysqli原生预处理也安全,但需显式调用bind_param(),类型字符(s/i/d/b)必须严格匹配 - 不要混用:比如用
PDO::quote()拼接 SQL,它只是加引号+转义,仍属字符串拼接,不防注入
为什么 htmlspecialchars() 和 strip_tags() 对 SQL 注入完全无效
这两个函数作用于 HTML 输出上下文,用来防 XSS;它们对 SQL 解析器毫无意义。数据库收到的是 HTTP 请求体里的原始字符串,不是浏览器渲染后的 HTML。
常见误用:
立即学习“PHP免费学习笔记(深入)”;
- 把用户输入先过
htmlspecialchars()再塞进 SQL —— 数据库看到的是zuojiankuohaophpcnscriptyoujiankuohaophpcn这种字面量,既没防注入,又污染了存储内容 - 在 SQL 查询前用
strip_tags()清洗 —— 同样只是删 HTML 标签,' OR 1=1 --这类 payload 完全不受影响 - 以为“前端加了校验就安全”—— 前端 JS 可被绕过,后端必须独立验证 + 参数化
动态表名/字段名不能参数化,必须白名单硬编码
PREPARE 语句只允许参数化「值」,不允许参数化「标识符」(如表名、列名、ORDER BY 字段、LIMIT 偏移量)。试图用问号占位符替换表名会直接报错:
PDOException: SQLSTATE[HY093]: Invalid parameter number
正确做法是:提取用户意图(如排序字段),映射到服务端预设的白名单中。
- 错误:
$sql = "SELECT * FROM ? WHERE status = ?";→ 语法错误 - 错误:
$table = $_GET['table']; $sql = "SELECT * FROM {$table}";→ 直接执行任意表 - 正确:
$allowed_tables = ['users', 'orders', 'products']; $table = in_array($_GET['table'], $allowed_tables) ? $_GET['table'] : 'users'; - 同理,
ORDER BY字段必须限定为['id', 'created_at', 'name']等明确列表,不可拼接用户输入
使用 PDO 时别忽略 PDO::ATTR_EMULATE_PREPARES 设置
MySQLi 默认走真实预处理,PDO 默认开启模拟预处理(即 PHP 层做字符串插值),虽仍比手动拼接安全,但存在极少数边界绕过可能(尤其旧版 MySQL + 多字节编码)。生产环境应关闭模拟:
$pdo = new PDO($dsn, $user, $pass, [
PDO::ATTR_EMULATE_PREPARES => false,
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
]);注意:关闭后若遇到 SQLSTATE[HY093] 错误,大概率是参数个数/顺序/绑定方式不对,不是配置问题。
另外,PDO::quote() 仅用于生成安全的字符串字面量(如动态构建 SQL 片段中的值),绝不能替代 prepare/bind 流程;且它不处理整数、NULL 等类型,需自行判断。
真正的防护不在过滤手段多寡,而在是否让数据库引擎彻底区分“语句结构”和“运行时数据”。其他所有“过滤”“替换”“截断”都是补丁,只有预处理是根治。











