2017年4月,漏洞缺陷一架从纽约飞往旧金山的漏洞缺陷联合航空客机上,一名乘客被暴力拖拽下机。漏洞缺陷事后调查发现,漏洞缺陷真正的漏洞缺陷问题不在乘务员的态度,而在美联航内部订票系统的漏洞缺陷座位状态同步逻辑——一个只有四行代码的判断分支写反了。这四行代码运行了十二年,漏洞缺陷没人在测试环境里触发过它。漏洞缺陷
这就是漏洞缺陷漏洞缺陷的典型面孔:它不总是惊天动地的崩溃,更多时候是漏洞缺陷一行写反的条件、一个漏掉的漏洞缺陷边界值、一段被遗忘的漏洞缺陷旧逻辑。它们藏在系统深处,漏洞缺陷像老房子墙里的漏洞缺陷白蚁,表面看不出什么,漏洞缺陷直到某天承重梁断了。
我做代码审查时有个习惯,专门盯着那些“看起来没问题”的地方。经验告诉我,越是程序员觉得理所当然的代码,越容易出问题。去年帮一个团队排查支付故障,他们反复检查了加密模块、网络超时、数据库事务,折腾了三天。最后发现是一个金额格式化函数在特定汇率下把“0.995”四舍五入成了“0.99”而非“1.00”——差了区区一分钱,但风控系统判定为异常交易,直接拦截了整个批次的支付请求。那个函数是五年前从Stack Overflow上抄来的,原作者在评论区早就标注了“不适用于金融计算”,没人看见。
缺陷的另一个来源是需求本身的模糊。产品经理说“用户登录后显示未读消息数”,开发实现为“查询消息表里is_read=0的记录数”。上线三个月后客服反馈:用户删除了未读消息,角标还在。开发说删除是逻辑删除,产品说删除就是删除。两人都没错,错的是“删除”这个词在需求文档里从来没被定义过。这类缺陷最难修,因为它不违反任何技术规范,只是违反了某个人脑子里的隐含假设。
更隐蔽的是环境缺陷。同一份代码在开发机上跑得好好的,到生产环境就间歇性超时。查来查去,是容器内存限制比本地少了256MB,触发了一个第三方库的内存回收bug。那个库的GitHub issue列表里,这个问题被标记为“wontfix”已经两年了。维护者说“你们的用法不对”,使用者说“你们的文档没写”。两边都在等对方先动。
我见过最昂贵的缺陷是一个分号。某银行的利息计算模块里,if (rate > 0.05); 后面多了一个分号,导致无论条件是否成立,下一行代码都会执行。这个模块每天处理几十万笔贷款计息,错误运行了十八个月。修复它只花了十秒钟,追回损失花了十个月。
安全领域有句话:漏洞是缺陷的一个子集,但缺陷不都是漏洞。一个拼写错误导致日志里少了个字段,这是缺陷,不是漏洞。一个整数溢出导致缓冲区被覆盖,这既是缺陷也是漏洞。区分它们很重要——修漏洞要快,修缺陷要准。快是因为攻击者不会等你排期,准是因为你永远不知道一个看似无害的缺陷会在哪个深夜变成P0故障。
写代码的人容易陷入一种错觉:测试通过了就等于没问题。测试只能证明你想到的场景没问题,证明不了你没想到的场景。那个写反了判断分支的美联航程序员,大概从来没想过会有人买了票又被要求下机——毕竟票都出了,座位就是乘客的。但系统不这么认为,系统只认代码里那行逻辑。
缺陷不会消失。系统越复杂,交互越多,隐藏的假设就越多,缺陷的藏身之处也越多。能做的是承认这一点,然后别把“没发现问题”当成“没有问题”。