优秀的程序员是什么样的?

在程序员的世界里有一句经典的至理名言:永远不要相信用户的输入。相信很多开发者都有过类似的踩坑经历,所谓“幸福的人都是相似的,而编程的人是幸福的”。

初入职时,作为一名后端工程师,每次实现接口后,老大(我的组长)都会过来帮我瞄一眼,然后提出一堆改进意见,其中最常被提到的一点就是:永远不要相信用户的输入。我不以为意,没怎么去改相关逻辑,这么做的后果就是老大下次再帮我检查时,总会伴随一阵悦耳的争辩声。

Round 1

你这里为什么不加个类型检查?万一接收到的值不是数组怎么办?

怎么会呢,这些接口类型都是和前端定好的呀。

前端传入的数据是可以被篡改的,万一有人模拟了一个前端请求,发送一个其他类型的变量,那你的程序不就炸了?

那样的话程序会返回服务器异常,攻击者也不会得到什么有用的信息吧…

是没什么损失,但为什么要给别人留下这么个漏洞呢?

哦哦…

Round 2

这个接口传入的是图片对吧,为什么不检查下图片的大小?

这个,前端已经检查过了,太大的图片前端会先进行压缩的。

不是和你说过了吗,永远不要相信前端的输入!万一有攻击者模拟前端接口发来很大的图片呢?

额,不是还有nginx可以挡一道吗?太大的图片应该直接无法响应吧。

nginx的确能做这个,但这些也是需要配置参数的,万一不小心参数设太大了没发现怎么办?

哦,那就只能接收并保存下来了,不过我们的图片不都放在s3上吗,大一点也没什么关系吧,存储也挺便宜的…

你太天真了!首先,s3也是要花钱的,虽然空间不贵,但流量贵啊,万一真被人发现这个漏洞,他们完全可以把大量的图片通过你这里上传,然后根据返回值里的图片链接去获取。甚至如果有不怀好意的人传了些不该传的东西上去,那咱说不定都要去局里坐坐,这可不是开玩笑。

Game Over

这下我算彻底服了,只好满口答应,然后老老实实把该补的补上。嘴上虽这么说,但心里难免还有点疙瘩,这些都是特殊情况嘛,哪有那么容易就发生。事情总是这样,只要还没在自己身上发生,就会抱有侥幸心理,认为它一定不会发生,等到真的发生了,又来责怪自己当初怎么那么不小心。笔者就有过这么个经历,虽没犯下什么大错,但也足够长点记性了。


话说最近在做微信公众号开发,某天PM(产品经理)姐姐拿着个新需求过来,说是要加一个批量导入历史用户的功能,我扫了一眼开发文档,在用户管理文档中找到了获取用户基本信息这一项,心想不正是这个接口么,于是自信地回了句:“明天下班前给你”,转头就热火朝天地敲起了键盘。

整个流程比较清晰,先通过接口调用凭据access_token获取用户的openid列表,再调用获取用户基本信息接口来得到用户的昵称等信息,再将获取到的信息存到数据库即可。不过,作为一个严谨的开发,当然不能这么草率,还要考虑下效率和安全的问题。微信公众号里的用户动不动就上万,要一个个去获取基本信息那绝对是不妥当的,这一点微信团队的大佬们当然也想到了,所以提供了一个批量获取微信用户信息的接口,甚是方便。

另外,将数据存储到数据库时,当然也不能一条条地存入,这样一不小心得把数据库整挂了。应该在程序里构造sql语句,累计到一定的用户量之后再以批量的方式插入,这样不仅减少了操作数据库的次数,而且直接执行sql语句的方式也更加高效。

考虑完这两点后,就开始敲代码了。手指翻飞间,几个接口函数已经基本完成,打开编译器,一路绿灯,又麻利地打开本地的前端服务,准备开始调试。点击导入历史用户按钮,后端收到请求,调试器里查看,微信正确地返回了用户列表,因为线下的环境只是个测试号,所以只有20人左右,继续点击下一步,用户信息也成功获取,下一步,sql语句执行成功,全部用户导入完毕,到数据库里一看,数据确实都存下来了,搞定!

接下来,就是将程序上线了。虽然只是个小功能,可一旦要上线都是需要找老大来的,毕竟万一线上的代码真出了问题这锅最后还得到他头上。于是我就跑去找来了他,他当然也知道我今天在开发这个功能,看我不到半天就完成了,还捎带夸了句:完成地挺快呵。我自然是轻扬起嘴角,淡淡地回了句:这活还蛮简单的。

然而,这种笑容很快就消失了,因为我看到坐在我电脑前的老大眉头紧缩,顿觉头顶乌云密布。果然,不到一会儿,就传来了老大的“盘问”。

你直接用sql语句来插入数据的?

嗯,感觉这样子更方便一点,效率也更快点。

这个想法是没错,但你这里有个问题啊,在构造sql语句的时候,要特别注意单引号的使用,你看你这里在每个变量的两边显式地加入了单引号,那要是变量的字符串里本身就包含单引号怎么办呢?

啊,变量的值怎么可能会有单引号啊,这一点我没想到诶。

怎么不可能?你这里的值是微信用户的信息对吧,万一哪个家伙无聊在昵称里加了个单引号呢,这完全是可能的,这种时候你的sql语句就会被这个单引号提前封闭,就会产生语法错误,也就无法正常地执行了。更要命的是,这个时候用户单引号后面的部分就成为了sql命令的一部分被执行了,万一来个drop database之类的,再加个单引号把原来的部分还原,那这就成了典型的注入攻击了。到时候你哭都来不及。

哇靠,原来还有这种操作啊,我如梦初醒,不觉后背发凉。之前听老大提起过在php时代著名的注入攻击的案例,没想到今天自己竟差点犯了这个错误,真是罪过。那该怎么解决呢?我向老大请教。

这个其实也挺简单,只要把可能存在的特殊字符给转义就行了,这样它们就不会变成sql的一部分被执行。

嗯,有道理。于是我就噼里啪啦改起来,不一会儿就把该转义的给转义了,老大看完微微点头,然后再次强调说,特别是涉及到数据库的操作时,一定要严格地检查用户的输入,考虑所有可能的情况,防止出现这样的问题。我毕恭毕敬地听着,点头如捣蒜。

不过事情还没完。老大紧接着又指出另一个问题:你在接收到微信服务器返回的用户信息列表时,有检查它的类型吗?

我有点疑惑,这个类型不是在微信的文档里写好的嘛,只能是JSON啊,难道这也需要检查吗?

老大貌似看出了我的疑虑,问到:万一用户的信息里确实有一些特殊的字符,没办法用JSON的方式传输呢?是不是需要先将JSON做个序列化再传输呢?而你没有判断返回值的类型,完全按照JSON的格式来处理,这样后面有可能会出错的。

我还是有点疑惑,不就是个昵称嘛,能有什么特别的字符呢?

老大看了看我,建议我现在去试下微信修改昵称的功能。这不看还真不知道,在改昵称的时候原来可以插入表情的啊,还不止是微信官方提供的表情,而是能添加自己私藏的任意表情,表情是海量的,而JSON能识别的字符集是有限的,这么一来也就自然会出现一些JSON无法识别的字符。

看到这,我算是相信了,于是又把类型检查给加上了,并且当接收到的类型是字符串时,对其进行了处理,消除了里面可能存在的非JSON字符。老大看了看我,这次算是露出了比较满意的表情。

最后,又检查了一遍,为了验证刚才老大的两个想法,我还特意加了一个记录日志的操作,看看接收到的信息到底都是些啥。确认无误后,老大把程序更新到了线上。两三分钟后,更新完成,我迫不及待地尝试了下这个新功能。说来您还真别不信,线上用户的公众号总共有两万多关注者,这些人里面居然真的有人在昵称里使用了单引号,而且这类人还不少,大概三四百人里就有一个。而且返回的用户信息列表居然真的不全是JSON格式的,而是有部分string格式的,将这些string解析后得到的JSON里面,确实包含了一些无法识别的字符。

我看着日志里的结果,又看了看微信公众平台的官方文档,再回头看看老大那挂着浅浅笑容的脸颊,不禁肃然起敬。一方面是觉得尽管诸如微信这样的官方文档也难免会有疏漏之处,不可一味地根据主观意向来判断。再者,则是对老大的远见卓识佩服之至。我记得曾在知乎上看到过一个问题,大意是说

做一个优秀的程序员到底难在哪里?

答案里有一条是这么写的:

由于你是一个优秀的(或仅仅是经验丰富的)程序员,你可以看出项目代码里存在着的隐患。你选择防患于未然,修复这些问题,但由于问题并没有真的发生,你所做的一切,在不那么优秀的程序员同事的眼中(以及老大眼中),看起来并没有什么产出。

诚哉斯言。回想起先前的经历,要不是老大及时指出代码里可能存在的错误,那肯定是要出问题的,虽然这一次不一定导致什么严重的后果,但若问题不除,迟早要吃大亏。此时老大已经起身准备离开了,他还有一大堆的事情要忙呢。不过在临走前,他又一次嘱咐了我:永远不要相信用户的输入。

嗯,这次我是真的记住了。望着老大远去的背影,我在心里默念到。

大概,这就是我心目中优秀程序员该有的样子。


参考链接

做一个优秀的程序员到底难在哪里? - Van Bruce的回答 - 知乎

如果您阅读本文后有所收获,不妨打赏两块钱,您小小的支持都是对作者莫大的鼓励!