php的自动类型转换导致的bug

今天发现一件很奇怪的事情.一些不在测试名单里的用户进入了我们游戏的测试版本并使用了一些尚未开发的功能.

我的第一反应是有人把这个测试版本误操作设置为正式版本了, 可是我查看了版本配置文件的修改日期, 和昨晚发布测试版本的时间吻合,排除了这个可能.

难道是有玩家通过什么漏洞知道我们测试版本的版本号了?于是再检查nginx的访问日志,并没有发现异常访问.那几个进入测试版本的用户也都是走正常的方式进入游戏页面的.我把用户访问页面的参数复制下来,模拟用户登录,发现果然是进入了测试系统.基本确定是系统在判断测试用户的逻辑上有bug了.

其实那段代码很简单

function isTestingUserId($userId){
    $testingUids = $this->getConfig('testingUids');
    return in_array($userId,$testingUids);
}

出现异常的userId是"0E93618",在配置文件里面定义的testing_uids数组里面没有这个字符串,于是就修改代码一个个尝试
发现了一个和它"相同"的userId:"0E676150", 在php里面执行下面的代码,会输出bool(true)

var_dump("0E93618" == "0E676150");

原因很明显, 是php在执行比较前做了数据类型的自动转换,导致比较操作出现了不想要的结果,如果使用===,不进行类型转换,那么就会输出bool(false)

var_dump("0E93618" === "0E676150");

is_array有第三个参数, 表示是否检查类型(和===一样,需要类型相同且值相同). 所以上面的代码中的in_array多加一个参数就好了

function isTestingUserId($userId){
    $testingUids = $this->getConfig('testingUids');
    return in_array($userId,$testingUids,1);
}

问题是解决了,但是真正的原因是什么,为什么 "0E93618" == "0E676150" 呢? 在php里面,0开头的数字会被认为是八进制, 可是8进制里面不会有'E'和'9'.而且下面的代码输出也是bool(true):

var_dump(0E93618 == 0E676150);

这说明代码执行的时候,php确实是先把字符串转换成数字了.于是,我再测试

var_dump(0E93618);
var_dump(0E676150);

输出是:

float(0)
float(0)

到这里,我才猛然醒悟,'E'啊,科学计数法啊!! 换成小写的e可能更显眼一点

0e93618 = 0 * 10^93618 = 0 = 0 * 10^676150 = 0e676150

userId以0E开头且后面的字符全是数字时, 之前的代码都会误认为两个用户id相同,

var_dump("0E93618" == "0E12345"); // 会做类型转换,输出 bool(true)
var_dump("0E93618" == "0E123A5"); // 第二项不是科学计数法形式, 不会做类型转换, 输出 bool(false)

在实际情况中, userId是由[0-9a-zA-Z]字符集中的字符组成的,出现这种userId的概率并不高. 所以之前一直没有发现这个隐藏的bug...

Posted via UltraBlog.vim.


Last modified on 2011-07-13