Press "Enter" to skip to content

深入理解SET NAMES和mysql(i)_set_charset的区别

最近公司组织了个PHP安全编程的培训, 其中涉及到一部分关于Mysql的"SET NAMES"和mysql_set_charset (mysqli_set_charset)的内容:
说到, 尽量使用mysqli_set_charset(mysqli:set_charset)而不是"SET NAMES", 当然, 这个内容在PHP手册中也有叙及, 但是却没有解释为什么.
最近有好几个朋友问我这个问题, 到底为什么?
问的人多了, 我也就觉得可以写篇blog, 专门介绍下这部分的内容了.
首先, 很多人都不知道"SET NAMES"到底是做了什么,
我之前的文章深入MySQL字符集设置中, 曾经介绍过character_set_client/character_set_connection/character_set_results这三个MySQL的"环境变量", 这里再简单介绍下,
这三个变量, 分别告诉MySQL服务器, 客户端的编码集, 在传输给MySQL服务器的时候的编码集, 以及期望MySQL返回的结果的编码集.
比如, 通过使用"SET NAMES utf8", 就告诉服务器, 我用的是utf-8编码, 我希望你也给我返回utf-8编码的查询结果.
一般情况下, 使用"SET NAMES"就足够了, 也是可以保证正确的. 那么为什么手册又要说推荐使用mysqli_set_charset(PHP>=5.0.5)呢?
首先, 我们看看mysqli_set_charset到底做了什么(注意星号注释处, mysql_set_charset类似):

//php-5.2.11-SRC/ext/mysqli/mysqli_nonapi.c line 342
PHP_FUNCTION(mysqli_set_charset)
{
    MY_MYSQL            *mysql;
    zval                *mysql_link;
    char                *cs_name = NULL;
    unsigned int        len;
    if (zend_parse_method_parameters(ZEND_NUM_ARGS() TSRMLS_CC, getThis()
		, "Os", &mysql_link, mysqli_link_class_entry, &cs_name, &len) == FAILURE) {
        return;
    }
    MYSQLI_FETCH_RESOURCE(mysql, MY_MYSQL*, &mysql_link, "mysqli_link"
        , MYSQLI_STATUS_VALID);
    if (mysql_set_character_set(mysql->mysql, cs_name)) {
		//** 调用libmysql的对应函数
        RETURN_FALSE;
    }
    RETURN_TRUE;
}

那mysql_set_character_set又做了什么呢?

//mysql-5.1.30-SRC/libmysql/client.c, line 3166:
int STDCALL mysql_set_character_set(MYSQL *mysql, const char *cs_name)
{
  struct charset_info_st *cs;
  const char *save_csdir= charsets_dir;
  if (mysql->options.charset_dir)
    charsets_dir= mysql->options.charset_dir;
  if (strlen(cs_name) < MY_CS_NAME_SIZE &&
     (cs= get_charset_by_csname(cs_name, MY_CS_PRIMARY, MYF(0))))
  {
    char buff[MY_CS_NAME_SIZE + 10];
    charsets_dir= save_csdir;
    /* Skip execution of "SET NAMES" for pre-4.1 servers */
    if (mysql_get_server_version(mysql) < 40100)
      return 0;
    sprintf(buff, "SET NAMES %s", cs_name);
    if (!mysql_real_query(mysql, buff, strlen(buff)))
    {
      mysql->charset= cs;
    }
  }
  //以下省略

我们可以看到, mysqli_set_charset除了做了"SET NAMES"以外, 还多做了一步:

sprintf(buff, "SET NAMES %s", cs_name);
if (!mysql_real_query(mysql, buff, strlen(buff)))
{
  mysql->charset= cs;
}

而对于mysql这个核心结构的成员charset又有什么作用呢?
这就要说说mysql_real_escape_string()了, 这个函数和mysql_escape_string的区别就是, 它会考虑"当前"字符集. 那么这个当前字符集从哪里来呢?
对了, 你猜的没错, 就是mysql->charset.
mysql_real_string在判断宽字符集的字符的时候, 就根据这个成员变量来分别采用不同的策略, 比如如果是utf-8, 那么就会采用libmysql/ctype-utf8.c.
看个实例, 默认mysql连接字符集是latin-1, (经典的5c问题):

<?php
    $db = mysql_connect('localhost:3737', 'root' ,'123456');
    mysql_select_db("test");
    $a = "\x91\x5c";//"慭"的gbk编码, 低字节为5c, 也就是ascii中的"\"
    var_dump(addslashes($a));
    var_dump(mysql_real_escape_string($a, $db));
    mysql_query("set names gbk");
    var_dump(mysql_real_escape_string($a, $db));
    mysql_set_charset("gbk");
    var_dump(mysql_real_escape_string($a, $db));
?>

因为, "慭"的gbk编码低字节为5c, 也就是ascii中的"\", 而因为除了mysql(i)_set_charset影响mysql->charset以外, 其他时刻mysql->charset都为默认值, 所以, 结果就是:

$ php -f 5c.php
string(3) "慭\"
string(3) "慭\"
string(3) "慭\"
string(2) "慭"

大家现在很清楚了吧?

31 Comments

  1. […] 为什么这个例子可以完成注入,答主也给了解释,原因就是上面所说的,pdo会进行模拟预处理,这样会在本地进行转义,具体转义可能类似于mysql_real_escape_string()或者PDO::quote()(PHP 5.5.0 起已废弃,并在自 PHP 7.0.0 开始被移除),此类函数和addslashes这种函数区别在于,它会读取当前pdo驱动设置的字符集,而set names gbk并没有设置这个值,至于为什么,看鸟哥这篇文章,所以该函数在转义时还是认为pdo驱动的字符集是latin或者是utf8,于是它给x27转义,结果就变成了xbf5c27(x57对应ascii的),该而数据库服务器的字符集被设置成gbk,于是xbf5c27被解释成 […]

  2. Wilson
    Wilson November 3, 2014

    如果我的mysql连接字符集是utf8,然后,,
    <?php
    $db = mysql_connect('localhost:3737', 'root' ,'123456');
    mysql_select_db("test");
    $a = "\x91\x5c";//"慭"的gbk编码, 低字节为5c, 也就是ascii中的"\"
    mysql_set_charset("utf8");
    var_dump(mysql_real_escape_string($a, $db));
    也会出现问题哦???

  3. emerald_zj
    emerald_zj July 18, 2012

    我用PDO都是直接用SET Names UTF8

  4. […] 另参见这一篇文章http://www.laruence.com/2010/04/12/1396.html 明白mysql_real_escape_string()的用法了。 未分类mysqli_real_escape_string, rt, 技术, 问答 ← imagecreatefromjpeg()问题 – 技术问答 插入排序问题 – 技术问答 → 发表评论?0 条评论。 […]

  5. 荒野无灯
    荒野无灯 November 6, 2011

    嗯,关于gbk编码与\的一个漏洞以前在某老外的blog上看到过。

  6. asdf1234
    asdf1234 April 17, 2011

    大牛,你是在什么公司啊,百度?

  7. 寻仙人
    寻仙人 March 7, 2011

    高手啊,膜拜

  8. blues
    blues September 3, 2010

    新手参上
    竟然深入到C中了,C语言没怎么学会,看了个不大懂,自学的时候也没注意到这些问题,见识了,你的博客都很有深度啊,会经常来看的.目前还只能做做应用,无法深入到C,你这篇文章改变了我对php的理解,本以为可以不学C了,现在发现要深入学习还是需要知其根源,希望2年后毕业的时候也能有这样的深度.

  9. OllieNoel
    OllieNoel July 3, 2010

    Have no enough cash to buy a car? Worry no more, just because that is achievable to get the credit loans to solve such problems. Thus get a small business loan to buy everything you need.

  10. Yufeng
    Yufeng April 18, 2010

    学习了,呵呵。

  11. aleafs
    aleafs April 13, 2010

    太清楚了

  12. 好黑啊
    好黑啊 April 13, 2010

    非常good的文章,我very like

  13. cz
    cz April 13, 2010

    所以这两年我一看到SET NAMES的代码就要破口大骂,虽然utf-8编码里不存在5c问题
    另外对于PDO,可以用prepared statement来解决。这也是一个比字符串过滤更好的防止注入的方法

  14. 雪候鸟
    雪候鸟 April 13, 2010

    @Anders pdo找不到类似的方法,,,,,

  15. Anders
    Anders April 13, 2010

    之前遇到这个问题,但是 使用的是PDO, 不知道如何解决。

  16. Anders
    Anders April 13, 2010

    之前

  17. kevin
    kevin April 13, 2010

    addslashes(), str_replace()等等基于ascii编码的函数都会遇到这种问题。mysql_*系列有解决方案,别的有什么好的解决方案么?

  18. felix021
    felix021 April 12, 2010

    1L真狠。。。

  19. qywyh
    qywyh April 12, 2010

    看贴不回木jj,学习了,顶!

Comments are closed.