代码安全审查

主要参考 OWASP Secure Coding Principles

法则1:最小化被攻击区域

应用每增加一个新的功能从总体上讲就会增加系统的风险,安全开发的目标是通过减少可能被攻击的区域来降低整体风险。

例如一个Web应用的在线帮助模块新增了搜索功能,那么搜索功能就可能存在SQL注入漏洞,如果在线帮助被限制只允许授权用户使用,那么被攻击的可能性就被降低了。如果搜索功能输入的数据都会被统一的数据校验模块过滤,那么SQL注入的可能性也会显著降低。当然了,如果能通过优化用户体验等手段做到不再需要搜索功能,即便在线帮助功能在互联网上可以被公开访问,那么SQL注入的风险也是被彻底消除了的。

法则2:建立安全缺省值

为用户提供一个立即可用的用户体验有很多种方式,然而缺省情况下提供给用户的系统应该是安全的,无风险的。在允许的情况下应该是由用户自己决定是否需要降低安全等级,以达到某种特别目的。

例如,缺省情况下密码的有效期和复杂度两个功能应该是被启用的,如果用户想要更简单便利的使用应用程序,则可以允许用户关闭这两个功能,并由用户承担随之增加的风险。

法则3: 最小权限原则

最小权限原则提倡只给用户分配处理业务流程所需要的最小权限,只要能完成业务流程操作即可,不给过多的权限,这些权限包括但不仅限于用户在业务层面的权限和底层资源(例如磁盘文件、网络)访问许可等方面的权限。

Web应用开发中通常会采用多层结构,API层有不同的API服务器,如果前端只需要调用某一部分API,那么对其他API的调用则应该是不被允许的。例如公网可以访问的零售业务的前端不应该允许访问财务对账的API, 财务对账的API只允许财务的前端调用。

法则4:进行深度防御

深度防御法则建议当采用一种安全机制已经比较合理的情况下,提供更多的,可以从不同维度应对安全风险的防御措施会更好一些。深度防御措施可以使那些严重的安全问题非常难以被暴露出来,因此这些安全问题也不太可能会发生。

在进行安全编程时,深度防御法则可以表现为,采用多层次的数据校验,集中的审计(audit)控制,以及所有访问页面都要求登录等等。

例如,一个有安全瑕疵的管理模块如果只允许从内部管理网络访问,并且每次都检查管理员权限以及记录所有的访问、操作,那么就不太可能遭受匿名的攻击。

法则5:安全地出错

一般来说应用会因为很多原因导致发生错误而无法完成某项操作,那么,以什么样的方式应对异常或者错误决定着应用是否安全,请看以下列子

isAdmin = true;
try 
{   
    codeWhichMayFail();   
    isAdmin = isUserInRole("Administrator");
} 
catch(Exception e) 
{   
    log.write(e.toString()); 
}

那么一旦try代码块出现异常,isAdmin变量会保留默认值true,意味着当前用户被赋予了管理员的权限,很明显这是有安全风险的。

更安全的方式应该是

isAdmin = false; 
try 
{   
    codeWhichMayFail();   
    isAdmin = isUserInRole(“Administrator”); 
} 
catch(Exception e) 
{   
    log.write(e.toString()); 
}

法则6:不信任任何服务接口

很多组织都在利用第三方合作伙伴来处理一些业务,这些合作伙伴正在使用的安全策略以及所处的安全情形很可能和你不一样,不管他们是你的亲属还是一些比较大的供应商/合作伙伴,影响或者控制这些外部的第三方合作伙伴基本不可能。为此盲目的信任他们并不可靠。因为它们可能会被攻击者利用,为己方招来安全威胁,所有外部系统都必须被一视同仁的对待。

例如,某家互联网银行使用第三方来管理客户积分及礼品兑换,帮助维持客户忠诚度。第三方系统提供客户积分点数和可兑换礼品的列表,那么当银行系统读取第三方系统提供的数据并展现给最终用户时,应做到显示数据时不会有安全问题,比如跨站脚本,还应该保证数据是在合理的范围内,不能是负数也不能异乎寻常的大。

法则7:职责分离

对于欺骗控制,关键的一项措施就是职责分离。

例如,一个在线零售应用里的一些角色会拥有和普通用户不一样的权限级别,尤其是管理员,一般来说管理员不应该是应用里的业务用户。管理员可以关闭和启动起系统,可以设置密码策略,但是它不应该可以使用当前身份登录店面前端帮助别人进行代购。

法则8:避免朦胧的安全性

模糊的不清晰的安全是一种很弱的安全控制。如果系统里唯一的安全机制是这样的,那几乎肯定是要出安全问题的。当然这并不是说保护一些秘密是坏事,而是说系统安全控制的设计和逻辑应该是遵循公开的已知的法则。

举个例子,一个加密系统它所使用的加密算法是可以被公开的,需要保护好的是加密用的密钥。这个系统的安全不应该依赖于不让别人知道使用的加密算法,因为即便算法很强如果密钥很简单(比如111111)系统一样很不安全。

系统的安全也不应该依赖于把源代码隐藏起来。而是应该依赖于很多因素,比如合理的密码策略,深度防御,业务交易的限制,坚固的网络架构以及欺骗和审计控制。最实际的例子就是Linux, Linux的源代码是公开的,然而恰当地配置后Linux却是非常安全和健壮的操作系统。

法则9:简化安全实现

系统安全的实现并不是越复杂越好,实现越复杂,也对应的扩大了受攻击的区域,相反简洁到位的实现才是最合适的,因此要防止出现事倍功半的现象。

举个例子,某个应用因为客观原因无法实现http session,出于用户体验的考虑又不能让使用者每个页面请求都输入用户名和密码。很容易想到的办法是客户端存储用户名和密码,同时为了应对潜在的风险,又在客户端添加功能对密码进行管理。,这样的个解决方案弊端很多,比如加密密钥如何安全存放、更换,是否每个客户端使用不同的密钥,如果修改密码还涉及到客户端和服务器端同步的问题,如果不慎密码被泄露了,密码规则也会被别人知道。比较合适的方式是客户端只存token,这个token在用户输入用户名/密码验证成功后在服务器端生成,并传送一份至客户端存储,每次请求使用此token来认证。 Tonken会定期失效,定时更换。这样就可以省去客户端的复杂的实现,完全在服务器端控制,未来也可以比较容易的切换到http session。

法则10:恰当的修复安全问题

一旦一个安全问题被甄别出来,充分理解问题的根本原因以及开发出合适的测试用例来覆盖这些场景是非常重要的。需要确保修复安全问题的改动不会再引入新的问题。

例如,某应用初始使用md5生成哈希值来保护密码,上线运营一段时间后被脱库,团队紧急修复,将算法改为SHA2。但是由于测试不全,只测试了线上应用的使用场景,没有测到不常用的数据恢复场景,导致某次数据恢复后所有帐号不能登录系统。因为被恢复的数据密码哈希值依然是md5算法生成的,验证模块使用SHA2值和md5值做比对,由于两者不匹配导致校验失败。