很多技术研发不了解安全,也不重视安全,只有在自己的服务器被黑掉、被挂马、被脱裤才想起关注安全,但是这个时候,技术架构已经成型、代码已经在线上稳定运行,再亡羊补牢,改代码、改策略,往往成本巨大、而收效很低;所以,开发安全,从娃娃抓起……

一、什么是信息安全?

信息安全是一个庞大的概念,包含大量不同方向的分支技术,但是都涉及几个概念:

  • 机密性(Confidentiality):即保证信息在产生、传输、存储、使用等环节不会被泄漏、被恶意窃取。在技术上典型的实现方式就是加密算法。加密算法主要分对称加密和非对称加密,对称加密的加解密密钥是一样的,所以在密钥在存储、传输时会有一定泄漏的风险,但加解密效率会相对高一些。非对称加密的密钥不同,各自不会互相影响,所以相对安全,但同样的,效率会低一些。因此,也随之产生多种可变的方案,比如,使用对称加密算法,密钥通过非对称加密算法进行加密,可以在效率和安全性上取得一定的平衡。加解密是一门很深的学科,也是信息安全领域一个方向。
  • 完整性(Integrity):即保证信息在产生、传输、存储、使用等环节是真实完整的,不会被恶意篡改。在技术上典型的应用有数字签名、MD5、校验和等。其实在网络协议诞生的初期就已经存在完整性校验的概念,最典型的就是TCP/IP协议中各种报文的校验和。而现在互联网产品,尤其是移动端APP产品很多也都采用签名的策略,通过将接口传参、时间戳等进行一次签名,来防止业务数据在传输的过程中被篡改。
  • 可用性(Availability):即保证信息在产生、传输、存储、使用等环节不会被破坏损毁。在技术上典型的应用,安全方面当然就是防DDoS了,DDoS攻击成本低,效果好,无论大小企业,现在俨然已成为难题之一。DDoS又分很多种,防护起来十分复杂。

以上是信息安全的基本属性,即CIA属性。此外,后期还衍生出其他的属性,如可控性、不可否认性等,总之都是对信息安全概念的补充。

而安全人员的工作,尤其是做企业安全建设的工作,就是围绕保护数据安全的过程,在事前、事中、事后三个阶段,技术上建立扫描、发现、监控、防御、应急、加固等一系列措施,在管理上完善流程、制度、规范,从而使以上几个安全属性得到保障。

概念说了很多,落地到实际是什么样子呢,简单总结了一个安全架构图,比较全的涵盖了企业安全建设的要点。

以上是宏观层面,那具体到每个技术研发同学的身上,最常见的就是对各种安全漏洞、安全风险的处理修复。下面介绍开发过程中常见的安全风险点。

二、SQL注入

sql注入危害很大,也很常见,可以导致企业数据直接被泄漏出去。典型的sql注入漏洞是这样产生的:

void doPostHttpServletRequest request, HttpServletResponse response{
    JdbcConnection conn = new JdbcConnection();
    final String sql = "select * from product where pname like '%” + request.getParameter("name") + "%'";
    conn.execqueryResultSet(sql);
}

在sql中直接拼接了字符串,导致用户可以通过插入恶意代码来控制sql执行。比如这样:

select * from product where pname like '%name%';

如果name变量输入 qudian';drop database;// 就变成了

select * from product where name like '%qudian';drop database;//%';

那么怎么防御sql注入呢?最简单正确的方式就是预编译。 为什么用预编译,首先要了解sql注入的原理:

sql注入产生在数据库的编译阶段,拼接字符串时,sql和用户可控的数据部分拼接到一起,一次发送到数据库, 数据库编译时就会把sql指令和数据编译到一起,如果用户可控的数据部分有非法的命令,也会被数据库编译执行,这样就产生了sql注入。

而预编译的方式的简单原理是:

sql和用户可控的部分是分两次发给数据库的,第一次发sql指令,也就是上个例子的select * from product where pname like '%name%';, 数据库收到后先进行编译,第二次再发送数据qudian';drop database;//,此时数据库不会重新编译第一次收到的指令, 而是把指令和数据区分开,这样不论用户输入的是什么非法数据,数据库都会认为是数据部分,也就不会产生sql注入了。

上面的过程通过抓包可以看到。

预编译的一般简单写法:

// 正常查询
conn = createConnection();
String sql = "select name,password from manager where name=? and password=?";
stat = conn.prepareStatement(sql);
stat.setString(1, name);
stat.setString(2, password);
stat.executeQuery(sql);

// 模糊查询
conn = createConnection();
String sql = "select * from table where url like ?";
stat = con.prepareStatement(sql);
String data="data";
stat.setString(1, "%"+data+"%");
stat.executeQuery(sql);

三、跨站脚本攻击:XSS

很多人不重视XSS,觉得XSS没有大危害,只是能弹个窗有什么用?但实际XSS的危害甚至不弱于远程代码执行等。平时常见的XSS的危害场景:

  • 盗取cookie
  • 读取用户隐私
  • 蠕虫
  • DDoS
  • 钓鱼
  • 键盘记录
  • 执行代码
  • 等等

3.1 XSS是怎么插的?

举个简单的例子,通常正常的表单是这样的:

<input class='txtValue' type='text' name='name' value='<% query.value %>'/>

访问的URL是这样的:http://www.qufenqi.com/1.html?value=abc ,最后看到的效果是这样的:

<input class='txtValue' type='text' name='name' value='abc'/>

但是现实总是和理想有差距,黑客通常会这样的URL参数来访问:

http://www.qufenqi.com/1.html?value=abc'/><script>alert('hello!')</script><div '

最后得到的就会是

<input class='txtValue' type='text' name='name' value='abc'/><script>alert('hello!')</script><div ''/>

这样就造成了XSS。

这种现象是产生的原因是由于服务端对用户的输入没有做任何处理,因此在浏览器渲染时,用户输入的js代码就会执行。 既然知道了原因,就不难修复,只要让浏览器渲染时js代码不会执行就可以了。

3.2 解决XSS的常用方法

  • HTML标签之间:HTML实体编码;
  • HTML属性里:HTML属性编码&#xHH; (以&#x开头,HH则是指该字符对应的十六进制数字,分号作为结束符);
  • Javascript里:Javascrpt编码\xHH (以 \x 开头,HH则是指该字符对应的十六进制数字);
  • CSS样式里:CSS编码\HH (以 \ 开头,HH则是指该字符对应的十六进制数字);
  • URL里:URL编码%HH(以 % 开头,HH则是指该字符对应的十六进制数字);
  • JSON里:Response.setContentType(“appliaction/json”);
  • 富文本里:标签or属性过滤。

由于富文本中有需要用户提交一些html、js、css标签,因此不能直接处理,建议除可能必需保留的标签外,过滤其他危险标签。

  • dynsrc、src、action、href、background、bgsound、lowers、value、onmouse*
  • applet、blink、frameset、iframe、object、base、body、head、layer、style
  • basefont、embed、html、link、title、bgdound、frame、player、meta、script
  • vbscript、ms-its、firefoxurl、javascript、shtml、mocha、data、livescript

3.3 解决XSS常见的两种误区

3.3.1 用关键字过滤的方式

关键字过滤能解决一部分常见的XSS,但是不能完全避免,通过转义等很多方式可以绕过过滤的方法。

比如这是正常的XSS语句:

<script>alert(1);</script>

但是把它变换一种形式,依然能够达到效果,比如这样:

<script>\u0061\u006c\u0065\u0072\u0074(1);</script>

3.3.2 全局暴力转义

不论XSS输出在哪,都用一种方式转义。有时候开发会写一个全局的转义方法,之后不论什么时候在哪出现XSS,都调用这个方法,XSS是解决了,但可能在某些场景下,影响正常的内容呈现:过度转义。

四、跨站请求伪造CSRF

首先一个图简单明了的展现了CSRF的过程

简单的说就是在用户的某个网站cookie有效时,诱使用户去请求黑客构造好的恶意请求,就能在神不知鬼不觉的情况下进行攻击。

一个发生过的经典例子,某网站后台管理员更改密码的功能,没有校验目前使用的密码,可以直接设置新的管理员密码。 操作请求的参数只有一个新的密码,所以构造链接http://xxx.com/updatepass?newpass=badpass,诱使管理员去请求,就可以默默的改变管理员密码。

下一个问题就是如何诱使受害者去主动请求恶意的链接,方式多种多样,比如在自己的网站上插入这个链接,让用户访问你的网站; 在论坛里插入外链图片,图片链接是恶意的链接,这些都可以。

除此之外,CSRF结合其他漏洞更能达到惊喜的效果,前几年知名的新浪微博蠕虫刷粉丝,可以短时间增长大量的粉丝,就是CSRF和XSS在一起的功效。

那么防御CSRF的方案呼之欲出,目前主流的两种方案:

  • 让黑客不能伪造恶意的请求
  • 校验来源的请求是不是用户正常触发的
    • 第一点,通常使用token校验。
      • 第一步,用户登录时,服务端生成token,保存在session中。
      • 第二步,token可以放在表单中或者http请求头中。
      • 第三步,客户端带着token发请求给服务端,服务端校验token。这样通过客户端和session中的token比较,就可以得知请求是否合法。由于token是随机字符串,黑客无法获取,也就无法构造请求了。
    • 第二点,校验referer,这是一个比较简单的实现方式,通过校验referer白名单也可以起到防御CSRF攻击的作用。

      不过是这里有个坑,很多开发写正则来取referer,有时候就会造成各种绕过的姿势,比如www.qufenqi.com.baidu.com这样。因此在写正则的时候一定要注意。

五、登录注册安全风险

登录注册的风险点主要有四个:

  • 暴力破解
  • 撞库
  • 遍历注册用户
  • 批量注册

首先登录,三个必备的要素:用户名、密码、验证码;验证码可以是手机短信验证码或者图形验证码。

通过手机短信验证码既可以识别用户身份,为风控提供基础,又可以防护暴力破解、撞库等批量的攻击行为。 图像验证码则可以人机识别,防护暴力破解、撞库。引入了验证码机制同样引入了额外的安全风险,比如短信验证码的短信炸弹风险、图形验证码的可绕过、可识别等。 此外,也可以加入一些高级的安全策略,辅助分析防护安全风险,如异地登录提醒、记录非常用设备登录、校验用户历史行为。

这里简单说下校验用户历史行为:很多产品在设计需要校验身份的场景时,没有完全的考虑各类安全风险,一个常见的例子就是通过短信验证码来找回密码, 产品理想中的场景是短信验证码只能用户自己收到,所以可以确认用户身份,但实际有很多不可控的因素,比如,用户手机丢失的情况。因此,在注册登录点需要综合考虑各种情况。

当然,图形验证码如果设计开发的不当那也是形同虚设了。

一个完善的图形验证码流程是这样的:

  1. 客户端发起一个请求。
  2. 服务端响应并创建一个新的SessionID同时生成一个随机验证码。
  3. 服务端将验证码和SessionID一并返回给客户端。
  4. 客户端提交验证码连同SessionID给服务端。
  5. 服务端验证验证码同时销毁当前会话,返回给客户端结果。

如果整个流程中的某个环节处理不当,则会产生各种问题:

  1. 验证码不过期,可重复使用。这是比例最大的验证码安全风险,产生这种现象的主要原因是验证码的刷新是在前端进行,服务端的功能只有接收验证码判断对错,并没有sessionid的机制,这样只要拦截请求每次使用这个验证码重新发包,就可以绕过验证码的策略做各种攻击尝试。正确的做法是每次校验验证码之后,服务端要重新生成验证码。
  2. 验证码输出到客户端。这种问题也很常见,很多验证码的逻辑是在请求验证码时,服务端不只返回验证码图片,还返回图片里的内容,这样通过抓取返回中的验证码内容字段就可以绕过验证码的人机识别过程。争取的做法是服务端返回时,不要返回验证码内容,直接返回一张图片即可。
  3. 验证码前端生成,前端校验。这里涉及安全的一个原则:永远不要信任用户端的输入。所有前端的代码都是可以被用户修改的,因此如果在前端做验证码处理,黑客则可以通过修改前端代码自己生成,自己校验,完全绕过验证码的逻辑。
  4. 验证码可以被识别。这是目前验证码的一个难题。现在图像识别的技术非常成熟,前端的数字字符验证码,即使增加了背景、干扰、粘连等措施,也可以被轻松识别。因此验证码技术现在逐渐发展成通过用户行为识别和找不同来做人机识别。比如滑动验证码、12306那类的验证码。

六、第三方系统的安全

很多产品避免不了和第三方产品的互相调用,但是在调用过程中如果不注意安全控制,很容易因为第三方系统的安全问题,导致自己的安全风险。

以前遇到过一个例子,某电商网站扩展二手回收业务,和某二手回收网站合作,会将自己的一些用户信息传给二手回收的系统,结果因为对方的安全做的不够完善,导致自己的大量用户信息被泄漏。 造成了很严重的影响。但是第三方的系统安全我们是控制不了的,因此我们只能互相调用的接口传输过程中加入安全策略。

通常有以下几种做法:

  • IP访问控制(白名单):这个是必需的,通常此类接口调用都不涉及很多的范围,都是双方之间的调用,需要做访问控制来限制恶意来源的访问扫描。
  • 接口签名:签名也是现在普遍的做法,通过签名可以确定接口传输的信息没有被恶意篡改。签名的简单逻辑是:签名串=MD5(明文参数&密钥),然后将签名串作为参数与原来的参数一起发给服务端。服务端收到明文参数后,同样进行一次MD5(明文参数&密钥),并于收到的签名串做比较,即可校验是否被篡改。现在的签名技术已经相对完善。
  • 敏感数据加密:敏感数据一定不要直接明文在互联网传输,要通过加密。加密算法可以选择对称加密和非对很加密,加密算法可以选用对称加密的AES或者非对称加密的RSA,或者二者综合使用。

简单的介绍了线上常见的几种安全风险,接下来说说几点安全基本的原则,也是各种安全方案的思想。 通过这几个原则可以扩展出很多成熟的安全方案:

  • 不要信任用户的输入:从用户端传过来的任何数据都是不可信的无论是请求头还是请求体,都是可以随便更改的。因此,这里边可能包含大量的恶意代码。用户传过来的数据一定要做一些关键的过滤、校验。对参数的类型、长度等一定要有预期,不符合预期的要做处理。此外,关键的算法、逻辑操作和数据不要在js中处理。js是可以随意更改的。一个典型的例子,大转盘抽奖一般是通过js实现,于是中奖的规则也在js中一起实现,js判断之后直接将中奖结果通知服务端,这样黑客就可以通过js随意控制抽奖的过程和结果了。
  • 合理利用加密、签名:加密和签名是安全策略中不可或缺的一部分。关键敏感的数据和接口一定要做加密和签名,加密和签名算法的选择又是一个庞大的话题,以后可以单独细说。
  • 关键操作的身份认证:关键操作不做身份认证就好像不问别人是谁就让他进你家门。在做身份认证时注意的几个要点:第一不要直接通过传参数来判断身份,比如userid,这是可以随意更改的;第二不要通过cookie中的某个字段判断身份,cookie中的信息也是可以随意更改的。正确的做法是通过session来获取用户身份。
  • 逻辑步骤:有时候有些逻辑步骤本可以一个步骤完成,却分成两个步骤,这种情况就有可能绕过第一个步骤直接判断第二个。举个简单的例子,登录时候输入用户名、密码、短信验证码,有的开发会先通过一个请求判断短信验证码是否正确,之后根据结果再发送请求。这时如果直接发送第二个请求就可以绕过短信验证码的校验,产生安全风险。因此,无论需要检验的参数数量有多少,都需要在一个步骤中做好所有的校验,之后再返回最终结果。
  • 策略一致性:现在的产品大多有多个平台,比如web端和app端。此时这两个平台的安全策略需要完全一致,才不会有疏漏。举一个以前遇到的例子,XSS的处理,在web端做输入过滤,在app端做输出转义,此时单独在两个平台都无法XSS,但是如果在app端输入,在web端输出,就恰好绕过两个平台的安全策略,最终成功XSS。因此,多个平台的产品,需要多个平台保持一致的安全策略才行。

想要说的也就这么多,其实很多问题没有深入,只是做了总结介绍,很多风险点由于篇幅关系也没有介绍,最后提醒大家:一定要注意线上安全!!!

作者:魏崇峰 - 趣店技术工程部-安全工程师
微信公众号(长按二维码关注)
加入我们

趣店(原趣分期)技术学院,重点关注技术架构、服务化、优秀工具、自动化平台、开发全流程一体化解决方案、新人培养、工程师进阶之道等方面。

如果你对我们感兴趣,欢迎【深入了解一下】