ThinkPHP本身不自动防御宽字节攻击,关键在于关闭PDO模拟预处理(PDO::ATTR_EMULATE_PREPARES=false)、统一显式字符集(如charset=utf8mb4)、禁用字符串拼接where、原生SQL必须用命名占位符绑定。

ThinkPHP 本身不自动防御宽字节攻击,关键取决于你是否关闭了 PDO 的模拟预处理(PDO::ATTR_EMULATE_PREPARES)以及是否使用了正确的字符集配置。宽字节注入在 ThinkPHP 中只会在特定组合下生效:MySQL 低版本 + GBK 字符集 + 开启模拟预处理 + 字符串拼接查询。
为什么宽字节攻击能在 ThinkPHP 中生效
宽字节攻击不是 ThinkPHP 框架本身的漏洞,而是底层 MySQL + PDO 配合不当导致的“绕过转义”现象。典型路径是:
- 数据库连接指定
charset=gbk(或gb2312),且未显式禁用模拟预处理 - 用户输入类似
%df%27这样的双字节序列,其中%df在 GBK 下是一个合法首字节,与后续的%27(单引号 URL 编码)组合成一个有效字符,使反斜杠转义失效 - 若代码用了
mysql_real_escape_string类逻辑(如旧版 ThinkPHP 的escape_string),而 PDO 又开启了PDO::ATTR_EMULATE_PREPARES = true,就会退化为字符串拼接,触发宽字节绕过
必须关闭 PDO 模拟预处理
这是防御宽字节攻击最硬性的前提。ThinkPHP 不会默认关掉它,你得手动配置。
- 在数据库配置文件(如
config/database.php)中,明确设置:'PDO::ATTR_EMULATE_PREPARES' => false - 不要依赖框架默认值,尤其在 Docker 或低版本 MySQL(如 5.5/5.6)环境中,
emulate_prepared_statements默认常为true - 验证是否生效:执行一次原生查询并开启 SQL 日志,看生成的 PDO 语句是否含
PREPARE和EXECUTE,而非直接拼接
字符集必须统一且显式声明
不能只靠 SET NAMES gbk,也不能让 PDO 自己猜。
立即学习“PHP免费学习笔记(深入)”;
- 连接 DSN 中必须带
charset=utf8mb4(推荐)或charset=gbk(不推荐,仅兼容旧系统) - 如果真要用 GBK,请同步确认 MySQL 服务端的
character_set_client、collation_connection全部为gbk,否则客户端和服务端编码不一致时,PDO 可能静默降级为模拟模式 - 避免在 SQL 里动态执行
SET NAMES—— 这类语句不走绑定,且无法被 PDO 预处理拦截
宽字节场景下仍要禁用字符串拼接 where
哪怕你已关掉模拟预处理,一旦回到字符串拼接写法,宽字节风险就重新暴露。
- 危险写法:
$user = Db::table('user')->where("name = '" . input('name') . "'")->find()—— 即使 PDO 是真实预处理,这里也根本没走绑定流程 - 安全写法优先用数组:
Db::table('user')->where(['name' => input('name')])->find() - 若必须用原生 SQL,务必用命名占位符 + 显式 bind:
Db::query("SELECT * FROM user WHERE name = :name", [':name' => input('name')]) - 特别注意:ThinkPHP 的
%s占位符(如where("name=%s", $name))在宽字节环境下不如数组可靠,因内部仍可能触发 escape_string 分支
宽字节攻击的隐蔽性在于它不报错、不抛异常,只是悄悄绕过过滤。最容易被忽略的是:你以为关了模拟预处理就万事大吉,却没检查 MySQL 实际返回的 character_set_client 是否真的匹配 DSN 声明——这两者不一致时,PDO 会自动 fallback 到 emulate 模式,整个防御链就断了。



















