宽字节sql注入本质是gbk等双字节编码下,0x5c反斜杠被前一高字节(如0xa1)组合成汉字而“被吃掉”,导致转义失效;utf-8中0x5c始终独立,不会被组合,故全程强制utf8mb4可根治该漏洞。

宽字节SQL注入不是“转义没做好”的问题,而是字符集错配导致的底层解析歧义。单纯把 mysql_real_escape_string 换成 mysqli_real_escape_string 或加 set names gbk 都可能失效——关键在连接层、传输层、存储层三者字符集必须对齐,且不能依赖 GBK。
为什么统一用 UTF-8 能堵住宽字节漏洞
宽字节注入本质是:当数据库连接使用 GBK(或其它双字节编码),而程序未显式声明连接字符集时,MySQL 会按 GBK 解析请求流;攻击者构造如 %A1%5C(即 0xA1 0x5C)这样的 URL 编码,其中 0x5C 是反斜杠,在 GBK 中若前一个字节 0xA1 是合法高字节,二者就组合成一个汉字,把本该用于转义的 \' 中的 \ “吃掉”,使单引号逃逸。
UTF-8 没有这种“高字节吃低字节”的机制:0x5C 在 UTF-8 中永远是独立的 ASCII 反斜杠,不会与前后字节组合成新字符。所以只要全程强制 UTF-8,0x5C 就老老实实参与转义,不再被“吞”。
但注意:只改 PHP 文件头或 HTML meta 不起作用,必须穿透到数据库连接层面。
PHP + MySQLi 必须设置的三个地方
以下任一环节漏掉,都可能让宽字节漏洞复活:
- PHP 连接时显式调用
mysqli_set_charset($conn, 'utf8mb4')(不是utf8,后者是 MySQL 的别名,实际只支持 BMP 字符) - 连接 DSN 或初始化语句中带
charset=utf8mb4,例如:new mysqli($host, $user, $pass, $db, $port, $socket, MYSQLI_CLIENT_FOUND_ROWS | MYSQLI_CLIENT_INTERACTIVE)后立即set_charset - 数据库表与字段的
COLLATION必须是utf8mb4_unicode_ci或类似,不能是gbk_chinese_ci—— 用SHOW CREATE TABLE users确认 - Web 服务器响应头需含
Content-Type: text/html; charset=utf-8,避免浏览器误判编码(尤其旧 IE)
Node.js + mysql2 的典型错误配置
很多项目写 charset: 'UTF8' 或 charset: 'utf8',这在 mysql2 中实际对应 MySQL 的 utf8(即 utf8mb3),不支持 emoji 和部分生僻字,更重要的是——它仍可能触发某些边缘宽字节解析路径(尤其配合 sql_mode=NO_BACKSLASH_ESCAPES 时)。
正确做法是:
- 连接选项中明确写
charset: 'utf8mb4' - 执行
SET NAMES utf8mb4作为初始化查询(即使已设 charset,也建议双保险) - 确保环境变量
MYSQL_CHARSET或配置文件里没覆盖这个值 - 检查
process.env.NODE_ENV是否在测试/开发环境误启用了GBK兼容模式
示例片段:
const pool = mysql.createPool({
host: 'localhost',
user: 'root',
password: 'pwd',
database: 'myapp',
charset: 'utf8mb4',
timezone: 'Z'
});
pool.query('SET NAMES utf8mb4');
容易被忽略的中间层陷阱
即便应用层全用 UTF-8,漏洞仍可能从中间件漏出:
- Apache 的
AddDefaultCharset GBK指令会污染所有响应,覆盖 PHP 的header() - Nginx 若配置了
charset gbk;或缺失charset utf-8;,静态资源或代理响应可能带错 header - CDN(如 Cloudflare)若开启“自动重写”或“优化编码”,可能把 UTF-8 响应悄悄转成 GBK 再发给后端
- 某些 ORM(如旧版 Sequelize)默认不传
charset,需在dialectOptions显式补上{ charset: 'utf8mb4' }
验证是否真生效:在数据库里执行 SELECT @@character_set_client, @@character_set_connection, @@character_set_results;,三者都应返回 utf8mb4;再查 SHOW VARIABLES LIKE 'collation%';,确认 collation 匹配。
最麻烦的不是设错,而是设了一半——比如连接层是 UTF-8,但某张日志表建表时用了 DEFAULT CHARSET=gbk,后续从这张表读出的数据再拼进新 SQL,就又回到宽字节风险区。











