堆叠查询攻击致命在于突破“只读”边界,可一次执行DROP/INSERT/UPDATE等写操作;根本防御是连接初始化时禁用mysqli_multi_query或PDO::MYSQL_ATTR_MULTI_STATEMENTS,而非过滤分号。

堆叠查询攻击之所以致命,根本原因在于它突破了“只读”边界——一次注入就能执行 DROP、INSERT、UPDATE 甚至系统命令,而防御者常误以为“过滤分号”就安全了。
mysqli_multi_query() 是 PHP 中唯一默认开启堆叠执行的函数
MySQLi 驱动本身默认禁用多语句,mysqli_query() 遇到分号直接报错:MySQLi SQL syntax error;但 mysqli_multi_query() 不仅允许分号分隔,还跳过对第二条语句的语法校验,全交给数据库执行。这意味着:
- 攻击者输入
1; DROP TABLE users --,前端可能只显示第一条 SELECT 的结果,但第二条已静默执行 - 旧项目常用它批量建表+插初始数据,导致代码里藏着未被审计的堆叠调用点
- 禁用后不会降级为单语句执行,而是直接抛出
Unable to execute multiple queries异常
PDO::MYSQL_ATTR_MULTI_STATEMENTS = true 是隐形开关
PDO 默认关闭多语句(PDO::MYSQL_ATTR_MULTI_STATEMENTS => false),但很多框架配置或手动连接时显式设为 true,等于主动打开后门:
- Laravel 的
config/database.php中若存在'options' => [PDO::MYSQL_ATTR_MULTI_STATEMENTS => true],必须删掉 - 即使启用了预处理(
PDO::ATTR_EMULATE_PREPARES = true),只要这个 flag 开着,分号仍可触发堆叠 - 错误日志里往往只记
SQLSTATE[HY000]: General error: 2014 Cannot execute queries while other unbuffered queries are active,不提示是多语句被禁用所致
WAF 拦分号根本不可靠,绕过方式太多
依赖中间件过滤 ; 属于典型“治标不治本”:
- URL 编码:
%3B替代分号,WAF 规则没解码就匹配会失效 - 注释混淆:
;/**/DROP/**/TABLE,空格和注释块让正则难以覆盖 - 非 HTTP 接口直连:CLI 脚本、定时任务、管理后台 API 完全绕过 WAF
- 真正有效的防线只有一处:连接初始化时,让驱动根本不具备执行多语句的能力
最易被忽略的是兼容性断裂点——安装脚本、数据库迁移工具、CMS 初始化流程里那些一次性执行的 DDL+DML 堆叠逻辑,禁用后不是变慢,而是直接失败。它们通常不在主业务链路里,排查时容易归因为“环境问题”,实际却是堆叠能力被掐断后的必然表现。


















