人工代码评审(由胜任的评审人员进行)仍然被认为是对安全最有好处的。但是,逐行地对大型应用的整个代码库进行人工评审是非常费时的,需要高技能的人力资源才能正确进行。这种方法的成本自然比用自动化工具扫描应用更高。如果资源有限,人工代码评审最好只在应用最关键的组件上进行。
提示 在检查代码之前,依靠开发团队自身(假定团队成员都是训练有素的)对各自的工作进行对等的代码评审可以作为增加人工代码评审覆盖面的补充方法。
我们前面已经提到过,“关键”最好在威胁建模过程中(从DFD中应该相当明显地看出来)定义。一些经典的人工代码评审关键点包括:
·直接接收或者处理用户输入的任何模块,特别是数据净化例程和与网络或者数据存储接口的模块。
·验证组件
·授权/会话管理
·管理/用户管理
·错误和异常处理
·加密组件
·以过度的权限/跨越多个安全上下文运行的代码
·可能受到流氓软件调试或者篡改的客户端代码
·历史上有过漏洞的代码
人工代码评审过程在其他资源中已经有了大量文档。我们最喜欢的一些在本章结尾处的“参考与延伸阅读”小节里列出。接下来,我们将讨论一些代码评审中出现的常见Web应用安全问题的示例。
代码评审识别的常见安全问题
代码评审可以识别出许多影响安全的问题。在这个小节中,我们将提供与Web应用关系最密切的一些示例,包括:
·低质量的输入处理
·低质量的SQL语句构成
·在代码中存储机密
·低质量的授权/会话管理
·在生产版本中遗留测试代码
低质量输入处理示例: 我们最喜欢反复重申的安全编码原则之一是“接收到的所有输入在被证明为清白之前应该作为恶意输入对待。”在Web应用中,要考虑的关键输入包括:
·从客户端接收的所有数据
·SQL语句或者存储过程接收的数据
·来自不可信来源的所有数据
在本书中我们已经看到,不能实施围绕这些数据的正确输入校验和输出编码例程会造成应用中毁灭性的安全漏洞。下面是在代码级别识别这些问题的方法的一些示例。
在前面的威胁建模的讨论中提供的购物车示例中,如果从客户端接收的用户名没有进行编码并且回显给客户端(一般在用户登录之后回显),在用户名域就可能进行一次XSS攻击。如果用户名没有进行编码并且传递给SQL,就可能产生SQL注入。因为许多Web数据用表单收集,代码中第一个要识别的就是输入页面中的<form>标记。然后,你就可以识别数据的处理方式。我们列出了一些应用服务器填写的HttpRequest ASP.NET对象属性,这样请求信息可以由Web应用编程访问:
·HttpRequest.Cookies
·HttpRequest.Form
·HttpRequest.Params
·HttpRequest.QueryString
一般来说,输入和输出应该进行净化。净化例程应该在代码评审期间仔细检查,因为开发人员在实现了某种校验或者需要接收广泛的输入时,会假定自己对输入攻击完全免疫。我们在第6章中深入讨论了输入校验对策,下面是输入校验例程中应该查找的一些常见示例:
·用“白名单”代替“黑名单”(黑名单容易被击垮——预测所有恶意输入实际上是不尽可能的)。
·对于用Java编写的应用,常常使用Java内建的正则表达式类(java.util.regex.*)或者Apache Struts框架的Validator插件。除非你的应用已经使用了Struts框架,否则我们建议坚持使用java.util.regex类。
·.NET提供正则表达式类进行输入校验(System.Text.RegularExpressions)。.NET框架也有Validator控件,提供与Struts框架的Validator插件等价的功能。这个控件的属性允许你配置输入校验。
下面是一个用ASP.NET框架中的Validator控件组的RegularExpressionValidator控件检查电子邮件地址的一个例子:
E-mail: <asp:textbox id = "textbox1" runat="server"/> <asp:RegularExpressionValidator id = "valRegEx" runat="server" ControlToValidate = "textbox1" ValidationExpression = ".*@.*\..*" ErrorMessage = "* Your entry is not a valid e-mail address." display = "dynamic">* </asp:RegularExpressionValidator>
代码中输入校验问题的一些好例子在第6章中加以阐述。
低质量的SQL语句构成示例: 在第7章中你已经看到,SQL语句是大部分Web应用的关键。编写不恰当的动态SQL语句可能导致对应用的SQL注入攻击。例如,在下面列出的select语句中,没有进行校验(输入或者输出)。攻击者可以简单地在密码域中注入'OR'1'='1(使SQL条件语句为真)获得应用的访问权。
<% strQuery = "SELECT custid, last, first, mi, addy, city, state, zip FROM customer WHERE username = '" & strUser & "' AND password = '" & strPass & "'" Set rsCust = connCW.Execute(strQuery) If Not rsCust.BOF And Not rsCust.EOF Then Do While NOT rsCust.EOF %> <TR> <TD> <cTypeface:Bold>Cust ID :</B> <% = rsCust("CUSTID") %></TR> </TD> <TR> <TD> <cTypeface:Bold> First </B><% = rsCust("First") %> <% = rsCust("MI") %> <cTypeface:Bold> Last Name</B> <% = rsCust("Last") %> </TR></TD> <% rsCust.MoveNext %> <% Loop %>
在存储过程中使用exec()也可能导致SQL注入攻击,因为'OR'1'='1仍然可以对存储过程进行SQL注入攻击,如:
CREATE PROCEDURE GetInfo (@Username VARCHAR(100)) AS exec('SELECT custid, last, first, mi, addy, city, state, zip FROM customer WHERE username = ''' + @Username ''') GO
SQL注入攻击可以通过进行合适的输入校验,尽可能使用参数化查询(ASP.NET)或者预处理语句(Java)来预防。
代码中的机密示例: Web开发者经常在代码中存储机密。你将在本章稍后的“二进制分析”小节中看到一个特别严重的例子,这个例子将展示坚决反对在代码中硬编码机密的原因。机密绝不应该存储在代码中。
如果存储机密是绝对必要的(例如用于非易失性的凭据存储),它们应该加密。在Windows上,应该使用数据保护API(DPAPI)加密机密并存储加密这些机密的密钥(链接参见本章结尾处的“参考与延伸阅读”小节)。Java加密扩展(Java Cryptography Extension,JCE)程序库自带的keystore可以用于在Java环境中存储加密密钥。
代码中授权错误的示例: 我们在第5章中已经看到,Web开发人员往往试图实施自己的授权/会话管理功能,导致应用访问控制中可能存在漏洞。
下面是代码评审中看到的低质量的会话管理的一个例子。在下面的例子中,userID是个整数,也被用作session ID。userID还是User表的主键,从而使开发人员很容易跟踪用户状态。在成功登录时,session ID被设置为与userID相等。
<!-- The code is run on welcome page to set the session ID = user ID --> Response.Cookies["sessionID"].Value = userID;
在后续页面中为了维护状态,从客户端请求该session ID,根据这个session ID向客户端回显相关的内容:
<!-- The following code is run on all pages --> String userID = (String)Request.Cookies["sessionID"];
在这个例子中,userID存储在客户端的Cookie中,因此,暴露在简单的篡改之下,可能导致会话劫持。
对定制会话管理来说,显而易见的对策是使用现有的会话管理例程。例如,会话ID应该用流行的开发框架中的会话对象创建,例如Java EE提供的JSPSESSIONID或JSESSIONID,或者ASP.NET提供的ASPSESSIONID。应用服务器如Tomcat和ASP.NET提供久经考验的会话管理功能,包括可在web.xml和web.config中配置的在特定的不活动期之后使会话到期的选项。许多平台还提供了更高级的授权例程,例如Microsoft的授权管理器(Authorization Manager,AzMan)或者ASP.NET提供的启用基于角色访问控制(RBAC)的IsInRole。在Java平台上,许多框架都提供基于配置的RBAC,如Apache Struts。
低质量的会话管理对于应用的数据层可能有更深远的影响。继续我们前面的例子,我们假定来自cookie的userid传递给一个SQL语句,这条语句执行一个查询并返回与各自的userid相关的数据。这个方案的代码如下:
String userId = (String)cookieProps.get( "userid" ); sqlBalance = select a.acct_id, balance from acct_history a, users b " + "where a.user_id = b.user_id and a.user_id = " + userId + " group by a.acct_id";
这是相当经典的SQL语句连接,盲目地装配来自用户的输入,据此执行查询。对这样的SQL连接逻辑应该始终非常仔细地进行检查。
显然,前面关于使用存储过程和参数化查询代替原始SQL连接的建议在这里是适用的。但是,我们还要强调这个例子中授权的影响:简单地篡改客户端的cookie userid值将使攻击者获得对其他用户敏感信息的访问,在这个例子中是其他用户的账户余额。为了避免这类授权问题,会话ID管理应该由成熟的应用框架或者应用服务器进行,比如Microsoft的.NET框架或者Tomcat应用服务器,或者在数据库级别创建内存中的临时表来实现。后者一般不能很好地扩展到大的应用中,所以前者最为流行。
访问控制也可以使用各种框架如Java校验和授权服务(Java Authentication and Authorization Service,JAAS)和ASP.NET实现(参见“参考与延伸阅读”小节)。
生产应用中测试代码的示例:Web应用中最古老的代码级安全漏洞之一是在生产部署中留下测试或者调试功能。常见的例子是提供调试参数查看关于应用的附加信息。这些参数通常在查询串上或者作为cookie的一部分发送:
if( "true".equalsIgnoreCase( request.getParameter("debug") ) ) // display the variable <%= sql %>
如果调试参数设置为“true”,整条SQL语句将在客户端显示。这个问题的另一个相似的例子是isAdmin参数。将这个值设置为“true”将授予应用的管理员等价权限,实际上导致了一次垂直权限提升攻击(参见第5章)。显然,调试/管理员模式开关应该在应用部署到生产环境之前删除。