MySQL在GBK编码下的5C问题

1 缘由

前段时间在折腾discuz的PHP版本升级,据说PHP7比PHP5的性能提升了很多,于是新建了一个论坛镜像,将其中PHP版本从5.x升级了7.x版本,将原来跑PHP5的容器替换为PHP7的容器,docker在做升级软件的时候确实很方便,不会影响宿主机环境。从统计数据看,测试论坛的平均响应时间确实缩短了20%左右,效果不错。只是第二天有同事反馈,说论坛有用户发帖称自己的用户名变成乱码了。

2 查证

刚刚收到这个问题的时候,有点疑惑,看到这个用户名中有繁体字”誠“,想着可能是繁体字的问题,于是在测试环境中发了个帖子,帖子内容就是这个用户名,发现果然显示乱码了,查了下数据库表,发现存储的内容是截去了部分字节。接着用其他的几个繁体字测试了下,发现居然没有乱码,看来不是所有繁体字都会导致乱码。

一开始是比较困惑的,只是升级了PHP版本,按理不会引起这种BUG才对,这看起来是MySQL的编码设置的问题,也就是character_set_client, character_set_connection, character_set_result这几个变量设置错误导致的。于是,就去查看discuz在新旧版本的PHP版本下数据库连接代码的不同的地方。最终发现discuz升级到PHP7以后,用的是source/class/db/db_driver_mysqli.php下面的连接代码,这里与PHP5使用的source/class/db/db_driver_mysql.php略有不同,PHP7用的新的 mysqli 库,PHP5用的是老的 mysql 库,两个库本身的功能是兼容的,但是数据库连接这里的编码设置发现了一些不同之处。

#PHP5采用的连接代码,dbcharset在我们系统中是GBK
$dbcharset = $dbcharset ? $dbcharset : $this->config[1]['dbcharset'];
$serverset = $dbcharset ?  'character_set_connection='.$dbcharset.
    ', character_set_results='.$dbcharset.', character_set_client=binary' : ''; 
$serverset && mysql_query("SET $serverset", $link);

#PHP7采用的连接代码
$link->set_charset($dbcharset ? $dbcharset : $this->config[1]['dbcharset']);
$serverset .= 'character_set_client=binary';
$serverset && $link->query("SET $serverset");

乍一看似乎是一样的,都是设置了 character_set_client=binary,然后将其他几项编码设置为GBK。(注意这里的character_set_client=binary设置,这是很重要的一个设置,在GBK编码的数据库中,MySQL的相关编码如果设置不对很容易引起宽字节注入,这个设置项就是为了防止SQL的宽字节注入的。关于宽字节注入这篇文章有很详细的说明 http://www.freebuf.com/articles/web/31537.html)

回到我们的问题,对比两个版本的数据库字符集设置的代码,由于PHP7使用了新的mysqli库,它是通过 set_charset()函数来指定的编码的,另外,还额外加了一个character_set_client=binary的配置。这个与PHP5的直接设置三个字符集的有所不同,set_charset是官方推荐的设置方式,从参考资料一可以知道,它除了SET NAMES xxx之外,还设定了escape_string时采用的编码mysql->charset。当数据库操作的时候,对变量转义会用到mysqli库的escape_string函数,即比如对于\, '这些字符,在数据库操作之前转义为\\, \'等。而新旧版本的不同之处在于,新版本的set_charset函数设置了escape_string所采用的字符集,而乱码问题恰恰是因为set_charsetcharacter_set_client=binary这两个设置混用导致的。

#set_charset函数部分代码
sprintf(buff, "SET NAMES %s", cs_name);
if (!mysql_real_query(mysql, buff, strlen(buff)))
{
  mysql->charset= cs;
}

创建一个测试表如下:

CREATE TABLE `post` (
  `idx` int(11) DEFAULT NULL,
  `content` varchar(60) DEFAULT NULL
) ENGINE=MyISAM DEFAULT CHARSET=gbk;

测试代码如下:

<?php
$link = new mysqli();
$link->real_connect('localhost', 'dztest', 'dztestpasswd', 'enctest',   null, null, MYSQLI_CLIENT_COMPRESS);

#老版本的编码设置,最终数据没有乱码
mysqli_query($link, "SET character_set_connection=gbk,  character_set_results=gbk,character_set_client=binary"); 

$v = "\xd5\x5c\xca\xb5"; // "誠实" 的GBK编码
$v1 = $link->escape_string($v);
echo "$v1\n"; // output: “誠\实"

#新版本编码设置,最终产生乱码
$link->set_charset('gbk'); 
$link->query("SET character_set_client=binary");

#在set_charset后,escape_string会考虑当前字符集GBK.
$v2 = $link->escape_string($v);
echo "$v2\n"; // output: “誠实"

$sql = "INSERT INTO post values(1, '$v1')";
$ret = mysqli_query($link, $sql);
$sql = "INSERT INTO post values(2, '$v2')";
$ret = mysqli_query($link, $sql);
?>

如测试代码中所示,对于带有繁体字的 誠实,它的GBK编码为\xd5\x5c\xca\xb5,注意到编码中的字节0x5c对应的ASCII字符就是\。在下面的示例代码中,输出1是誠\实,也就是说escape_string只是将誠实当做普通的ASCII字符处理,将\xd5\x5c\xca\xb5转义成了\xd5\x5c\x5c\xca\xb5,而并不考虑当前的字符集编码为GBK,因为没有设置escape_string用到的字符集mysql->charaset为GBK。恰巧又有character_set_client=binary,于是mysql在编码转换的时会进行类似unescape处理,最终存储到数据库的是正确的誠实,通过SELECT hex(content) FROM post,查看发现字段内容为d55ccab5,没有乱码。

而在新版本的这种设置方式下,输出2是誠实,也就是说在escape_string的时候考虑了当前字符集为GBK,因为我们通过set_charset("GBK")设置了escape_string用到的字符集mysql->charset=GBK。转义后还是\xd5\x5c\xca\xb5,而由于character_set_client=binary,在mysql中由 character_set_client->character_set_connection->column character_set时,即binary->gbk->gbk时,会进行unescape,由于\x5c后面跟的是并不能unescape的字符,最终存储的数据变成了,它的GBK编码是d5ca。也就是说除了去掉\x5c,还把最后的\xb5截掉最后留下两个字节。

最终表的内容如下:

mysql> select * from post;
+------+---------+
| idx  | content |
+------+---------+
|    1 | 誠实  |
|    2 | 帐     |
?>

那要修复这个问题,有两种方案:

  • 其一是使用set_charset()来统一设定所有编码,而不要再额外添加 character_set_client=binary,但是这样的话就要保证代码中所有涉及数据库的变量的转义操作采用的是mysqli_escape_string,不要用addslashes以及mysql_escape_string这些不考虑字符集的转义函数,否则会有宽字节注入的风险。
  • 其二是如果老的系统中不能保证变量都正确转义了的话,则最好采用character_set_client=binary的方式,而不使用set_charset()函数。看起来有点阴差阳错,不过最终的编码恰好是正确的,这也是discuz在PHP5版本中采用的方式。

简单总结下,MySQL采用GBK编码在设置连接字符集的时候要当心,设置错误就可能会导致乱码或者宽字节注入漏洞,使用UTF8编码应该可以规避宽字节注入问题。另外提一点,GB2312也没有这个问题,GB2312的编码范围和GBK其实是不同的。

3 参考资料

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335

推荐阅读更多精彩内容