宽字节SQL注入的本质不是字符过滤不严,而是字符集转换过程中产生了逻辑漏洞。当数据库使用GBK、GB2312这类宽字节字符集时,一个汉字占两个字节,攻击者可以通过构造特殊字节序列,让反斜杠转义符被“吞掉”,从而闭合单引号,突破原本看似安全的过滤。修复这个问题不能只靠替换几个函数,必须从连接层、代码层、数据层三个维度同时下手。

先看漏洞触发的最典型场景。很多PHP程序在连接MySQL时会执行SET NAMES 'gbk',同时代码里用addslashes()或mysql_real_escape_string()对输入做转义。假设用户提交了%df',经过URL解码后变成0xdf和单引号。addslashes()会在单引号前插入反斜杠,变成0xdf、0x5c、0x27。但MySQL收到后按GBK解析,0xdf和0x5c正好组成一个合法汉字“運”,反斜杠被消耗掉,剩下的单引号就变成了字符串闭合符,SQL注入由此产生。这不是addslashes()本身失效,而是字符集不匹配导致转义语义被破坏。

修复第一步:统一连接字符集为UTF-8

最彻底的方案是让数据库连接、数据表、PHP内部处理全部使用UTF-8字符集。UTF-8是变长编码,单字节字符永远以0x00到0x7F表示,反斜杠0x5c不会被后续字节合并成多字节字符,转义语义天然安全。在PHP中使用PDO时,DSN里明确指定charset=utf8mb4,并且不要在执行SQL前再发送SET NAMES指令,避免字符集被覆盖。如果必须使用MySQLi,连接后立即执行$mysqli->set_charset("utf8mb4"),这个函数会同时更新客户端和连接的字符集状态,比直接query("SET NAMES utf8mb4")更可靠。

// PDO连接示例,字符集固定在DSN中
$dsn = 'mysql:host=127.0.0.1;dbname=test;charset=utf8mb4';
$pdo = new PDO($dsn, 'user', 'password', [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);

如果业务系统因为历史原因必须使用GBK,无法切换到UTF-8,那么就需要在转义前先做字符集转换。常见做法是先用iconv或mb_convert_encoding把输入从GBK转为UTF-8,在UTF-8环境下完成转义和过滤,再转回GBK写入数据库。但这种方法实现复杂,容易在多层转换中遗漏边界条件,只能作为过渡方案。

修复第二步:用参数化查询替代手工转义

无论字符集是什么,参数化查询都能从结构上消除注入可能。PDO的prepare和execute机制会把SQL语句结构和数据分开传输,数据部分不会被当作SQL语法解析。这意味着单引号、反斜杠、宽字节序列都只是普通数据内容,不存在闭合字符串或改变语法结构的可能。即使数据库使用GBK,参数化查询也不会触发宽字节注入。

// 参数化查询示例,用户输入完全作为参数绑定
$stmt = $pdo->prepare('SELECT * FROM users WHERE username = ? AND password = ?');
$stmt->execute([$username, $password]);
$user = $stmt->fetch();

对于无法使用PDO的旧系统,至少要用mysqli的prepare语句。不要再用addslashes()或mysql_real_escape_string()拼接SQL,这些函数在宽字节场景下存在天然缺陷。如果代码中仍然存在大量拼接SQL,建议优先重构数据访问层,把拼接逻辑集中替换为预处理语句。

修复第三步:输入过滤与输出编码的边界控制

参数化查询解决了SQL注入的核心问题,但输入过滤仍然不能完全放弃。对于明确格式的数据,比如数字ID、邮箱、手机号,在进入业务逻辑前用正则或类型强制转换做白名单校验。数字型参数直接使用intval()或filter_var()处理,字符串型参数限制长度和允许字符集。这种边界控制能拦截明显异常的请求,降低攻击面。

// 数字参数强制类型转换
$id = isset($_GET['id']) ? intval($_GET['id']) : 0;
if ($id <= 0) {
    exit('非法参数');
}

// 字符串参数白名单校验
$username = $_POST['username'] ?? '';
if (!preg_match('/^[a-zA-Z0-9_]{3,20}$/', $username)) {
    exit('用户名格式不正确');
}

输出编码是另一个容易忽略的环节。当数据库里已经存在攻击者早期写入的恶意内容时,前端展示必须做HTML实体编码,防止存储型XSS与SQL注入形成组合攻击。使用htmlspecialchars()处理输出内容,并指定ENT_QUOTES标志,确保单双引号都被转换。JSON输出场景使用json_encode()自带的转义机制,不要手工拼接JSON字符串。

修复第四步:数据库层加固与最小权限

应用层修复之外,数据库本身的配置也要收紧。应用连接数据库的账号不应该拥有FILE、DROP、CREATE等高风险权限,只授予SELECT、INSERT、UPDATE、DELETE等业务必需权限。即使发生注入,攻击者也无法通过LOAD_FILE读取系统文件或INTO OUTFILE写入webshell。MySQL的secure_file_priv参数应设置为空或指定安全目录,禁止任意路径的文件读写。

对于MySQL 5.5以上版本,建议开启sql_mode中的STRICT_TRANS_TABLES和NO_ENGINE_SUBSTITUTION,让非法数据处理更严格。同时定期审计慢查询日志和错误日志,宽字节注入攻击往往伴随大量异常字符出现在日志中,通过监控可以及时发现攻击尝试。

修复第五步:Web应用防火墙的规则补充

WAF可以在网络层拦截宽字节注入的典型特征。攻击载荷中常见的%df%27、%bf%27、%aa%27等编码序列,以及解码后出现的0xdf27、0xbf27等字节组合,都可以作为检测规则。但WAF不能替代代码修复,只能作为纵深防御的一层。攻击者可以通过双重编码、大小写混合、注释符插入等方式绕过规则,所以WAF策略需要定期更新,并结合实际攻击日志做针对性调整。

在部署WAF时,要开启对请求体、URL参数、Cookie、HTTP头的全面检测。宽字节注入不仅出现在GET参数中,POST表单、JSON请求体、甚至User-Agent头都可能被利用。对于JSON格式的请求,WAF需要先解析JSON结构再做字符集层面的检测,否则规则可能因为编码层级不同而失效。

修复第六步:历史数据清洗与漏洞复测

修复代码后,数据库中可能已经存在攻击者注入的恶意数据。需要对核心表做全量扫描,查找包含可疑宽字节序列的字段内容。重点检查用户表、文章表、配置表中是否出现0xdf27、0xbf5c等字节模式,以及是否有多余的SQL关键字片段。对于确认被污染的数据,根据业务逻辑做删除或还原处理。

修复完成后必须进行回归测试,使用原始攻击载荷验证漏洞是否真正关闭。测试用例要覆盖GBK和UTF-8两种连接场景,覆盖GET、POST、Cookie三种输入渠道,覆盖数字型、字符型、搜索型三种注入点。自动化扫描工具可以辅助发现遗漏,但不能完全依赖工具结果,手工构造的边界用例往往能发现工具覆盖不到的逻辑漏洞。

宽字节SQL注入的修复核心在于消除字符集转换与转义机制之间的冲突。统一使用UTF-8加参数化查询是当前最可靠的组合方案,数据库权限最小化和WAF规则补充则提供了额外的防御纵深。对于存量系统,优先推动连接字符集升级和SQL拼接代码重构,这两项工作完成后,宽字节注入的威胁基本可以消除。安全修复不是一次性动作,需要配合代码审计、日志监控和定期复测形成持续防护闭环。