第01章:快递打包员的故事——为什么计算机需要"序列化"?
系列化学习之php反序列化
今天聊一个听起来很唬人的词——"反序列化漏洞"。别被这四个字吓跑。
先别看代码。我们聊聊乐高。
你在淘宝上买了一套乐高千年隼,那个三千多块、7541块零件的变态套装。拼了一个月终于拼好了,摆在桌上威风凛凛。然后你想把它寄给成都的大学室友。
抱着千年隼去快递站,老板看了一眼:"兄弟你认真的?这玩意儿一米多长,快递盒都塞不下,路上颠两下零件全掉光。"
你想了想,也对。于是你把它拆了——按说明书一块一块拆开,塞进收纳袋。在袋子上贴张纸条:"千年隼号,75192 型号,7541 块零件,拼装说明请见乐高第 75192 号图纸。"然后塞进快递盒,寄走。
室友收到后,拆箱,看到收纳袋和纸条,翻出说明书,照着 7541 块零件的编号一块块拼回去——一小时后,千年隼又出现了。
这个"拆"和"拼",翻译成计算机术语,就是序列化和反序列化。
你拆乐高的时候做了两件事:把立体的模型压扁成可以装进盒子的零件,同时在收纳袋上贴了标签——这个标签很重要,没有它室友拿到一堆零件根本不知道这是什么、按什么图纸拼。
计算机序列化也是一样:把活的对象压成扁平化的字符串,同时在这串字符串里打上"标签"——类名、属性名、属性类型。对方拿到这串字符串,照着标签就能还原出对象。
你要是觉得乐高太玩具化了,换个更实在的:你玩《星露谷物语》,种了两百颗蓝莓。爽。该睡觉了,点"保存游戏"。
程序这时候在干嘛?
你那个农场、那个角色、那两百颗蓝莓——它们不是硬盘上某个文件里的静态数据,它们是"活"在内存里的程序对象。内存这东西有个毛病:一断电全清零。你辛辛苦苦种了一下午的蓝莓,关个电脑就全没了——不是因为游戏坏,是物理规律决定的。
所以程序得在关电脑之前,把这些还在内存里活蹦乱跳的对象"压扁"成一串能写到硬盘上的东西。压扁的动作就是序列化,写到硬盘就是存盘。
第二天打开电脑点"继续游戏"——程序从硬盘上把那串东西读回来,在内存里重新"充气",还原成那个有蓝莓的农场。的下午没有白费。
这就是序列化存在的全部意义。不是什么高端技术,就是一个朴素的物理问题:内存的东西会消失,得在它消失之前搬到硬盘上。而要搬家,就必须先把立体的东西压扁。
好了故事讲够了。写一行代码。
打开的 PHP,敲这个(手敲,不要复制粘贴,感受一下):
<?phpclassUser{public$name;public$age;}$user = newUser();$user->name = "张三";$user->age = 18;$packed = serialize($user);echo$packed;
运行 php test.php,会看到一行鬼画符:
O:4:"User":2:{s:4:"name";s:6:"张三";s:3:"age";i:18;}
第一次看到这串东西正常人都会皱眉,但它没那么可怕。我带一个字一个字看:
O — Object 的首字母,"注意了,接下来描述的是一个活对象"
:4: — "User" 这个词有 4 个字母
"User" — 类名,这个对象的图纸编号
:2: — 有两个属性:name 和 age
{ } — 大括号里装着属性的具体内容
s:4:"name" — s = String,属性名是 name,4 个字母
s:6:"张三" — 两个中文字,UTF-8 下一个字 3 字节,两个字 6 字节
坑点提醒:新手最常在这里踩坑——不是 s:2,是 s:6。不信用 strlen("张三") 跑一下。
s:3:"age";i:18; — age 有 3 个字母,i = Integer,值是 18
搞清楚了?反过来试一下——拿到这串鬼画符,怎么还原对象:
$string = 'O:4:"User":2:{s:4:"name";s:6:"李四";s:3:"age";i:20;}';$restored = unserialize($string);echo$restored->name; // 李四echo$restored->age; // 20
这就是反序列化——拿到"快递盒"(这串字符串),看标签(类名 + 属性描述),把对象还原出来。
PHP 的序列化还有个兄弟叫 JSON,可能更熟悉:
echojson_encode($user);// {"name":"张三","age":18}
JSON 也是序列化,但它和 PHP 序列化有个根本区别:JSON 只能描述数据,不能描述"这是一个什么类型的对象"。json_decode 回来的是一个 stdClass,不是 User。PHP 序列化反序列化回来的是真正的 User 对象——这就是它比 JSON 强的地方,也是它比 JSON 危险的地方。因为真正的对象是有"行为"的——有方法、有魔术方法。后面几章会看到,这个区别是"能控制数据"和"能执行代码"之间的分水岭。
顺便说一句:序列化不是加密。O:4:"User":... 念出来就像念身份证号一样——人不加密,机器也不加密。base64 也不是加密,解码就回来了。加密需要密钥,序列化和 base64 都没有这玩意儿。
这一章记住三件事就行:
- 1. 序列化 = 把活对象压扁成字符串。反序列化 = 把字符串还原成活对象。
- 2. PHP 的序列化字符串是明文、没有任何保护。谁拿到都能看懂,都能改。
- 3. PHP 序列化反序列化回来的对象是"活的"——有类、有方法、有可能会自动执行的魔术方法。这是它比 JSON 危险的根本原因。
下面几道作业,挑 2 到 3 个做就好:
- • 把上面的
serialize 和 unserialize 各手敲跑一遍,改改 $user->name 看字符串哪里变了。 - • 把自己的名字(中文的话)设成
$user->name,看 s: 后面是多少。换成英文名再试,对比中英文字节数差异。 - • 同一个 User 对象,分别用
serialize 和 json_encode 输出,对着两串字符串找差异。 - • 如果我胆子大:把序列化字符串里的
s:6:"张三" 直接改成 s:6:"黑客",反序列化看看能不能成功。想一下——如果序列化数据来自浏览器 Cookie,把 Cookie 里的"张三"改成了"admin",程序会怎么处理?
最后一个问题不是课后作业,是留给下一章的悬念。