前情
前五篇,主要考虑性能优化,只有第二篇与安全性相关。
其实区块链业界一直不缺乏黑客,最近看过不少安全事故导致惨重代价,所以想总结点安全性方面的优化思路。(本篇比较务虚,只是大体思路。)总的来说,为了安全是必须付出实现或者性能代价的。实现代价是开发、测试阶段就要投入更多精力,性能代价是因为考虑更多,有可能消耗更大运行资源。但从长期来看,这些代价都是必须的。
产品价值与安全意识
开发者可能有能力做一定安全防范,但如果他认为产品没有价值,没必要防范,就可能明明有能力防住,实际却被黑翻车。要不要注重安全性,是设计阶段就应该交代清楚的。
夫兵久而国利者,未之有也。故不能尽知用兵之害者,则不能尽知用兵之利也。——《孙子兵法》
做任何事情先考虑失败。——李嘉诚
程序员版解读:安全怎么能大意,甚至忽视?那都是侥幸心理,只要您的产品、服务有价值,长期看都会被破解、攻击。开发者如果不能知悉黑客可能的攻击点,并衡量被攻破的代价,他必然也不清楚自己写的代码的真正价值。
实际开发过程中,有些领导者会故意隐藏关于产品价值的信息,这实际上可能导致安全考虑不到位。这种情况就应该配备一个在安全方面经验丰富的审查者。
一句话总结:越有价值的东西,就越应该注重安全。
知识深度
一般黑客都是上层、底层皆通,尤其擅长底层。很少听说只做增删查改业务的人能够黑掉什么东西、偷到数字货币,因为同样只做增删查改业务的人就具备防止这种级别的攻击手段。
比如古老的 SQL 注入漏洞,即便是入门级的 Web 开发也能理解并防护,用预编译语句、存储过程、改用 ORM 就天然免疫。他们无法防护的往往来自更底层的 Web Server 的漏洞,比如 Apache、Nginx 某个版本有 bug,刚好中枪。
再举个例子,用 C/C++ 写 UDP 服务程序,“先把它实现,能用就行”,“不就 socket 嘛?很容易!”于是没有考虑 socket 等资源的生存周期,没料到黑客可以伪造 UDP 包源地址,实现出来的就可能有拒绝服务攻击 (Denial of Service,DoS) 漏洞。
总之,为了性能或安全的优化,开发者往往需要往底层钻。为性能,主要是研究底层模块与之配合,达到消除瓶颈目的;为安全,则是不让对底层设计的不了解,导致实现不够严谨周密而产生漏洞。
知识广度
经常听到这样的段子:
千万不要跟程序员说,你的代码有 bug。
他的第一反应是你的环境有问题,第二就是你是傻逼不会用吧!
你要跟他这么说:这个程序运行的怎么运行的跟预期不一样,是我操作有问题吗?
这货就会第一反应,我擦,这是不是出 bug 了?
这段子里其实间接反映一个程序员经常遇到的问题:自己测试没问题,一到用户侧或者线上就莫名出问题。环境不同,是最大原因。比如 Linux 的发行版众多;著名的 Android 碎片化;iPhone 型号随时间推移也越来越多了……
另一个大原因则是依赖。比如古老的 DLL Hell。类似的问题在 macOS、Linux 上也都存在。有一次 UMU 把 macOS 的 OpenSSL 升级到 1.1,结果 1.0 居然被删掉,导致原来编译的依赖 1.0 的 eos 就无法运行了。
再以 eos 为例,它依赖不少库,这些依赖库本身也可能有 bug,也要升级。又比如 ipfs,熟悉下来,发现其依赖树很广。作为开发者,引入一个依赖时,肯定需要操心会不会同时引入 bug。一般解决方式是:采用被大量验证的著名库,尽快跟进最新稳定版本。
总之,为了安全,设计者可能需要了解更多方面的知识,并不仅限于表面上需要的那些。开发者还要与时俱进,积极消灭潜在的漏洞。
开发语言
高级语言程序员可能很少听说缓冲区溢出,即使有,多半也是这门语言的宿主、解释器的 bug。而 C/C++ 等能直接操作指针的语言,就可能听到栈溢出、空指针、野指针等。采用高级语言确实会在程序执行时的安全性上省心不少,能把更多精力放在流程安全、业务安全上。
从架构上说,应该让不同语言只用于它擅长的领域。比如用 Rust 实现底层模块,用 Go 实现上层业务。这种分层选择语言的方式,充分利用 Rust 的安全性和底层开发能力,还可以让分工更清晰、沟通更愉快。