近期负责一个主站系统的Code Review,过程中排查了一些由于输入验证引起的安全漏洞,但遗憾的是遗漏了一个分支功能点,导致线上用户昵称可能存在被篡改的风险。打算总结分享一些安全方面的经常,所以有了这篇博客。

假设所有的用户输入都是攻击性的,所有的用户都具有恶意

如果理解这句话并在开发时注意到这一点,关于输入验证的安全问题已经没有过多需要阐述的了。之前几次考虑过写一篇输入验证相关的文章而没有写,就是觉得这个问题的答案过于简单:"所有用户输入都是恶意".不过这个简单的问题能玩出的花样实在很多,许多同事都中过踩过其中的雷.

一些原则

我不是专职的安全测试,一些关点还是从研发角度出发。如果想了解更详多的WEB安全可以看一下 OWASP 开发指导,包括了WEB开发中会涉及到的安全准则。

界定用户输入

http_log
在一次HTTP请求中,由HTTP协议承载的所有信息都是用户输入。所以HTTP的头和体中的信息都是可以伪造的,比如Get/Post参数,Cookie,Referer,UserAgent等等.
应该假设这其中所有的元素都可能具有攻击性,需要对其做出必要的验证,程序不能以它们为依据,轻意做出与安全资源相关的访问决定.由于在开发面对更多的是GET/POST参数,往往会习惯性忽略header中的其它信息。
 

攻击影响范围
attack_diagram

请求都是基于Http协议,所以对于输入难的边界应该是HTTP请求。而图中所有的黄色方块区域,表示恶意输入可能影响范围,可以大致分为三种:
HTTP响应
除了比较常见对响应体的攻击(如XSS),对于响应头的攻击也是值得重要视的(如CRLF注义)。
浏览器一些安全难都是对服务器所返回的响应头所决定的,比如X-XSS-Protection,Access-Control-Allow-Origin,其中还包含了一些将被用于再次发送的信息,如Cookie,对响应头的保护应该受到重视。
资源访问
数据资源的访问可以包括但不限于数据库(SQL注入),文件系统(未授权访问)和LDAP(注入)等系统中能涉及到的资源。
服务访问
服务访问可以分为内部服务和外部服务,则框架,工具,逻辑代码到容器的漏洞都是攻击的目标。而且这类攻击,往往对服务器本身的影响更加致命。
总结来说主要针对服务器响应,和服务器代码执行的攻击。
 
客户端验证不可靠,但不可或缺
系统防范的边界是HTTP协议请求:唯一和系统进行通信交流的部分.正常操作下HTTP请求是由浏览器代用户发出,但除了浏览器,HTTP请求可以轻意的通过工具伪造发出.浏览器/Javascript的验证不是一道可靠的防线.
但如果客户端能甄别出大多数正常用户的无效请求,避免将其发送到服务端从而减少后端服务器的压力,这是另一个方面的"系统安全".
 

不要相信HTTP头中的信息
不能使用HTTP中的头信息做出安全敏感的决策.HTTP头中的多数信息是由浏览器生成,比用户的表单相对"可靠".但也因此造成了一些开发的忽视,而不对HTTP头中的信息检查.而HTTP头信息中的如referer, host, cookies,X-FORWARD-FOR,X-REAL-IP等都是经常被使用的预定义/自定义头信息.
 
敏感COOKIE加密
Cookies经常被用作sesssion标识或者后端数据的临时转储.应用程序应该对其中敏感的数据做双向加密或者不可逆摘要加密,使其不发生碰撞或者轻意的破解.
 
使用SSL/HTTPS
敏感信息应当则https加密传输,避免被劫持造成明文信息泄漏.不应认为对发送的报文字段用JS进行客户端加密后是绝对安全的:客户端加密,既加密的方式完全暴露在客户端代码中.因此被劫持到之后,仍然可能通过客户端代码反推加密信息(如可从JS中代码中拿到salt,公钥等信息)
 

验证用户输入

数据类型/限制验证
这是最基础的验证: 对数据类型限制,边界,组合等进行验证,驳回明显不符合业务定义的请求。其中有些需要注意的细节可以参考安全编程: 验证输入
 
元字符/特殊字符验证
如之前所说,恶意的输入主要的攻击目标为:服务访问,数据访问及Http响应本身。验证应该过滤掉到对这三部份中使用到的特殊字符(一般是其保留字符),如XSS则当过滤掉或转义<,>,SQL注入应该使用预编译,文件/FTP访问时应该注意“/”,“.”,”~”等路径操作字符等。
 
资源权限验证
用户输入是随意伪造的,用户可以输入一个完全符合数据类型且不具备“技术”攻击性的参数。但如果功能涉及敏感资源的访问,则应该对资源标识符进行权限验证等。
权限的验证不能只停留在传统的权限模型上。 可以延伸到业务领域中。
举个例子:某网站的活动全是对外公开的,假定当活动完成后,该活动的所有信息均可被用户查看。而当用户恶意的嗅探,输入一个新建状态活动的标识符后,应该拒绝其访问。当然从另一个角度看,这算是业务逻辑验证的范畴。
 
业务逻辑验证
比如一个提现单,若用户输入了1000元,而实际帐户中只有100元,这此次难不通过。业务逻辑验证的目的是为了确定用户当前输入满足业务规则,满足当前的既定事实。
 
业务逻辑验证(2)
这里需要专门提一下业务验证的扩展思维,这是之前聚美遇到问题,一个转款单的金额为139100xxxx,眼尖的同学已经看出来的,这是一个电话号码。 这是单纯的前台运维输入的一个错误,由于是紧急转帐可以不用关联上游的业务实体。 对这个数据做不了确切的验证。 可以说这是业务设计的问题,但在无法改变业务设计的情况下处理这个问题,能有一些常识性验证,验证通过警告的形式提醒是比较好的。
 
校验逻辑的严谨性
上面只是笼统的介绍了检验的几个方面,而其实验证最重要一点便是逻辑的严谨性,这一两年已经很多次看到,有足够安全的意识,但处理中却出现逻辑漏洞的代码,举两个例子:
1.一个功能需求:通过前台传入参数判定是升序还是降序,1代表升序,2代表降序,并且排序SQL是动态拼装的。
if ("1".equals(order)){
     order = "asc"
}else if("2".equals(order)){
     order = desc
}
// executeQuery(param, order)
这段代码的问题很明显,当用户输入既不为1,也不为2的情况下,这个逻辑可以很轻意的绕开,比如直接输入”; delete table xx”.
对于这类验证,可以采用白名单,映射的形式限定输入合法值,这个后面再讲。
2.第二个功能需求:当企业中标后,可以查看中标项目相关的信息。
Long pId = extractFromRequest();
Long eId= extractFromSession();
Project project = loadProject(pId);
if(project.enterpriseId != null && !project.enterpriseId.equals(eId)){
     return PAGE_403;
}
// render page
return page;
这个验证的漏洞在于,如果这个项目并没有任何企业中标(既project.enterpriseId为空),则不能正确的返回403错误信息,而是正常渲染界面。
根据代码评审的经验,发现这种类型问题并不在少数,而这种错误往往也是最可惜的。
 
非法输入的验证方式
对于非法输入,可以采用的处理方式有
1.拒绝响应
2.转义过滤
3.映射过滤
4.范围限定及默认值
每种处理方式都有各自适应的场景,很好选择就不赘述了。说一下验证形式,输入验证验证方向应当是“业务功能只能接受哪些值”,而不是“哪些值是非法值”,如果是后者,出现遗漏的机率会大大增加。

一些扩展

重新界定用户输入
之前的话题我们把用户输入限定到了HTTP协议,实际上攻击者可能操控的资源更多。
例如:如果系统需要从一个外部系统获取某些参数输入,如通过WEB Service获取汇率,那攻击者可以通过dns劫持或者直接对外部服务系统攻击,达到感染本系统输入的目的;再比如攻击者能控制机器时间,那代码中的时间检测相关的功能需要重新审视(2000年左右,有不少软件付费可以通过修改系统时间达延长使用期)。
绝对的系统安全,应该验证所有恶意用户可能控制感染的输入源,不应限于恶意用户直接输入
 
关于安全测试
就我的经历而言,不应过多的信任安全部门的安全测试报告(误?),我之前也用appscan对系统做漏洞检测,这些工具最大的功能在于,发现已经的,存在于技术、工具、框架、容器等通用组件的安全漏洞,但对于业务上的漏洞,基本上无法嗅探出。可能检测工具有更多的配置项达到业务检测的目的,我没有学习过。不过从结果来看,通过安全部门测试的系统在业务上往往并不安全。反而,通过做code review更好。
不确定这个是工具的原因,还是是安全部门测试的不完善。但确定的是,目前对于安全测试的态度:必须做,但对结果不绝对信任,特别是业务漏洞。
关于最后一点,我并非安全测试专员,也没有见过比较好的测试部门,如果有说错,还望安全测试的朋友指正。