“安全”是个很大的话题,各种安全问题的类型也是种类繁多。如果我们把安全问题按照所发生的区域来进行分类的话,那么所有发生在后端服务器、应用、服务当中的安全问题就是“后端安全问题”,所有发生在浏览器、单页面应用、Web 页面当中的安全问题则算是“前端安全问题”。比如说,SQL 注入漏洞发生在后端应用中,是后端安全问题,跨站脚本攻击(XSS)则是前端安全问题,因为它发生在用户的浏览器里。
除了从安全问题发生的区域来分类之外,也可以从另一个维度来判断:针对某个安全问题,团队中的哪个角色最适合来修复它?是后端开发还是前端开发?
总的来说,当我们下面在谈论“前端安全问题”的时候,我们说的是发生在浏览器、前端应用当中,或者通常由前端开发工程师来对其进行修复的安全问题。
按照上面的分类办法,我们总结出了 8 大典型的前端安全问题,它们分别是:
老生常谈的 XSS
警惕 iframe 带来的风险
别被点击劫持了
错误的内容推断
防火防盗防猪队友:不安全的第三方依赖包
用了 HTTPS 也可能掉坑里
本地存储数据泄露
缺失静态资源完整性校验
由于篇幅所限,本篇文章先给各位介绍前 4 个前端安全问题。
1、老生常谈的 XSS
XSS 是跨站脚本攻击(Cross-Site Scripting)的简称,它是个老油条了,在 OWASP Web Application Top 10 排行榜中长期霸榜,从未掉出过前三名。XSS 这类安全问题发生的本质原因在于,浏览器错误的将攻击者提供的用户输入数据当做 JavaScript 脚本给执行了。
XSS 有几种不同的分类办法,例如按照恶意输入的脚本是否在应用中存储,XSS 被划分为“存储型 XSS”和“反射型 XSS”,如果按照是否和服务器有交互,又可以划分为“Server Side XSS”和“DOM based XSS”。
无论怎么分类,XSS 漏洞始终是威胁用户的一个安全隐患。攻击者可以利用 XSS 漏洞来窃取包括用户身份信息在内的各种敏感信息、修改 Web 页面以欺骗用户,甚至控制受害者浏览器,或者和其他漏洞结合起来形成蠕虫攻击,等等。总之,关于 XSS 漏洞的利用,只有想不到没有做不到。
防御 XSS 最佳的做法就是对数据进行严格的输出编码,使得攻击者提供的数据不再被浏览器认为是脚本而被误执行。例如<script>在进行 HTML 编码后变成了<script>,而这段数据就会被浏览器认为只是一段普通的字符串,而不会被当做脚本执行了。
编码也不是件容易的事情,需要根据输出数据所在的上下文来进行相应的编码。例如刚才的例子,由于数据将被放置于 HTML 元素中,因此进行的是 HTML 编码,而如果数据将被放置于 URL 中,则需要进行 URL 编码,将其变为%3Cscript%3E。此外,还有 JavaScript 编码、CSS 编码、HTML 属性编码、JSON 编码等等。好在现如今的前端开发框架基本上都默认提供了前端输出编码,这大大减轻了前端开发小伙伴们的工作负担。
其他的防御措施,例如设置 CSP HTTP Header、输入验证、开启浏览器 XSS 防御等等都是可选项,原因在于这些措施都存在被绕过的可能,并不能完全保证能防御 XSS 攻击。不过它们和输出编码却可以共同协作实施纵深防御策略。
你可以查阅 OWASP XSS Prevention Cheat Sheet_Prevention_Cheat_Sheet),里面有关于 XSS 及其防御措施的详细说明。
2、警惕 iframe 带来的风险
有些时候我们的前端页面需要用到第三方提供的页面组件,通常会以 iframe 的方式引入。典型的例子是使用 iframe 在页面上添加第三方提供的广告、天气预报、社交分享插件等等。
iframe 在给我们的页面带来更多丰富的内容和能力的同时,也带来了不少的安全隐患。因为 iframe 中的内容是由第三方来提供的,默认情况下他们不受我们的控制,他们可以在 iframe 中运行 JavaScirpt 脚本、Flash 插件、弹出对话框等等,这可能会破坏前端用户体验。
如果说 iframe 只是有可能会给用户体验带来影响,看似风险不大,那么如果 iframe 中的域名因为过期而被恶意攻击者抢注,或者第三方被黑客攻破,iframe 中的内容被替换掉了,从而利用用户浏览器中的安全漏洞下载安装木马、恶意勒索软件等等,这问题可就大了。
还好在 HTML5 中,iframe 有了一个叫做 sandbox 的安全属性,通过它可以对 iframe 的行为进行各种限制,充分实现“最小权限“原则。使用 sandbox 的最简单的方式就是只在 iframe 元素中添加上这个关键词就好,就像下面这样:
<iframe sandbox src="..."> ... </iframe>
sandbox 还忠实的实现了“Secure By Default”原则,也就是说,如果你只是添加上这个属性而保持属性值为空,那么浏览器将会对 iframe 实施史上最严厉的调控限制,基本上来讲就是除了允许显示静态资源以外,其他什么都做不了。比如不准提交表单、不准弹窗、不准执行脚本等等,连 Origin 都会被强制重新分配一个唯一的值,换句话讲就是 iframe 中的页面访问它自己的服务器都会被算作跨域请求。
另外,sandbox 也提供了丰富的配置参数,我们可以进行较为细粒度的控制。一些典型的参数如下:
allow-forms:允许 iframe 中提交 form 表单
allow-popups:允许 iframe 中弹出新的窗口或者标签页(例如,window.open (),showModalDialog (),target=”_blank”等等)
allow-scripts:允许 iframe 中执行 JavaScript
allow-same-origin:允许 iframe 中的网页开启同源策略
更多详细的资料,可以参考 iframe 中关于 sandbox 的介绍。
3、别被点击劫持了
有个词叫做防不胜防,我们在通过 iframe 使用别人提供的内容时,我们自己的页面也可能正在被不法分子放到他们精心构造的 iframe 或者 frame 当中,进行点击劫持攻击。
这是一种欺骗性比较强,同时也需要用户高度参与才能完成的一种攻击。通常的攻击步骤是这样的:
攻击者精心构造一个诱导用户点击的内容,比如 Web 页面小游戏,将我们的页面放入到 iframe 当中,利用z-index 等 CSS 样式将这个 iframe 叠加到小游戏的垂直方向的正上方把 iframe 设置为 100% 透明度,受害者访问到这个页面后,肉眼看到的是一个小游戏,如果受到诱导进行了点击的话,实际上点击到的却是 iframe 中的我们的页面点击劫持的危害在于,攻击利用了受害者的用户身份,在其不知情的情况下进行一些操作。如果只是迫使用户关注某个微博账号的话,看上去仿佛还可以承受,但是如果是删除某个重要文件记录,或者窃取敏感信息,那么造成的危害可就难以承受了。
有多种防御措施都可以防止页面遭到点击劫持攻击,例如 Frame Breaking 方案。一个推荐的防御方案是,使用X-Frame-Options:DENY 这个 HTTP Header 来明确的告知浏览器,不要把当前 HTTP 响应中的内容在 HTML Frame 中显示出来。
关于点击劫持更多的细节,可以查阅 OWASP Clickjacking Defense Cheat Sheet。
4、错误的内容推断
想象这样一个攻击场景:某网站允许用户在评论里上传图片,攻击者在上传图片的时候,看似提交的是个图片文件,实则是个含有 JavaScript 的脚本文件。该文件逃过了文件类型校验(这涉及到了恶意文件上传这个常见安全问题,但是由于和前端相关度不高因此暂不详细介绍),在服务器里存储了下来。接下来,受害者在访问这段评论的时候,浏览器会去请求这个伪装成图片的 JavaScript 脚本,而此时如果浏览器错误的推断了这个响应的内容类型(MIME types),那么就会把这个图片文件当做 JavaScript 脚本执行,于是攻击也就成功了。
问题的关键就在于,后端服务器在返回的响应中设置的 Content-Type Header 仅仅只是给浏览器提供当前响应内容类型的建议,而浏览器有可能会自作主张的根据响应中的实际内容去推断内容的类型。
在上面的例子中,后端通过 Content-Type Header 建议浏览器按照图片来渲染这次的 HTTP 响应,但是浏览器发现响应中其实是 JavaScript,于是就擅自做主把这段响应当做 JS 脚本来解释执行,安全问题也就产生了。
浏览器根据响应内容来推断其类型,本来这是个很“智能”的功能,是浏览器强大的容错能力的体现,但是却会带来安全风险。要避免出现这样的安全问题,办法就是通过设置X-Content-Type-Options 这个 HTTP Header 明确禁止浏览器去推断响应类型。
同样是上面的攻击场景,后端服务器返回的 Content-Type 建议浏览器按照图片进行内容渲染,浏览器发现有X-Content-Type-OptionsHTTP Header 的存在,并且其参数值是 nosniff,因此不会再去推断内容类型,而是强制按照图片进行渲染,那么因为实际上这是一段 JS 脚本而非真实的图片,因此这段脚本就会被浏览器当作是一个已经损坏或者格式不正确的图片来处理,而不是当作 JS 脚本来处理,从而最终防止了安全问题的发生。
5、防火防盗防猪队友:不安全的第三方依赖包
现如今进行应用开发,就好比站在巨人的肩膀上写代码。据统计,一个应用有将近 80% 的代码其实是来自于第三方组件、依赖的类库等,而应用自身的代码其实只占了 20% 左右。无论是后端服务器应用还是前端应用开发,绝大多数时候我们都是在借助开发框架和各种类库进行快速开发。
这样做的好处显而易见,但是与此同时安全风险也在不断累积——应用使用了如此多的第三方代码,不论应用自己的代码的安全性有多高,一旦这些来自第三方的代码有安全漏洞,那么对应用整体的安全性依然会造成严峻的挑战。
举个例子,jQuery 就存在多个已知安全漏洞,例如 jQuery issue 2432,使得应用存在被 XSS 攻击的可能。而 Node.js 也有一些已知的安全漏洞,比如 CVE-2017-11499,可能导致前端应用受到 DoS 攻击。另外,对于前端应用而言,除使用到的前端开发框架之外,通常还会依赖不少 Node 组件包,它们可能也有安全漏洞。
手动检查这些第三方代码有没有安全问题是个苦差事,主要是因为应用依赖的这些组件数量众多,手工检查太耗时,好在有自动化的工具可以使用,比如 NSP (Node Security Platform),Snyk 等等。
6、用了 HTTPS 也可能掉坑里
为了保护信息在传输过程中不被泄露,保证传输安全,使用 TLS 或者通俗的讲,使用 HTTPS 已经是当今的标准配置了。然而事情并没有这么简单,即使是服务器端开启了 HTTPS,也还是存在安全隐患,黑客可以利用 SSL Stripping 这种攻击手段,强制让 HTTPS 降级回 HTTP,从而继续进行中间人攻击。
问题的本质在于浏览器发出去第一次请求就被攻击者拦截了下来并做了修改,根本不给浏览器和服务器进行 HTTPS 通信的机会。大致过程如下,用户在浏览器里输入 URL 的时候往往不是从 https://开始的,而是直接从域名开始输入,随后浏览器向服务器发起 HTTP 通信,然而由于攻击者的存在,它把服务器端返回的跳转到 HTTPS 页面的响应拦截了,并且代替客户端和服务器端进行后续的通信。由于这一切都是暗中进行的,所以使用前端应用的用户对此毫无察觉。
解决这个安全问题的办法是使用 HSTS(HTTP Strict Transport Security),它通过下面这个 HTTP Header 以及一个预加载的清单,来告知浏览器在和网站进行通信的时候强制性的使用 HTTPS,而不是通过明文的 HTTP 进行通信:
Strict-Transport-Security: max-age=<seconds>; includeSubDomains; preload
这里的“强制性”表现为浏览器无论在何种情况下都直接向服务器端发起 HTTPS 请求,而不再像以往那样从 HTTP 跳转到 HTTPS。另外,当遇到证书或者链接不安全的时候,则首先警告用户,并且不再让用户选择是否继续进行不安全的通信。
7、本地存储数据泄露
以前,对于一个 Web 应用而言,在前端通过 Cookie 存储少量用户信息就足够支撑应用的正常运行了。然而随着前后端分离,尤其是后端服务无状态化架构风格的兴起,伴随着 SPA 应用的大量出现,存储在前端也就是用户浏览器中的数据量也在逐渐增多。
前端应用是完全暴露在用户以及攻击者面前的,在前端存储任何敏感、机密的数据,都会面临泄露的风险,就算是在前端通过 JS 脚本对数据进行加密基本也无济于事。
举个例子来说明,假设你的前端应用想要支持离线模式,使得用户在离线情况下依然可以使用你的应用,这就意味着你需要在本地存储用户相关的一些数据,比如说电子邮箱地址、手机号、家庭住址等 PII(Personal Identifiable Information)信息,或许还有历史账单、消费记录等数据。
尽管有浏览器的同源策略限制,但是如果前端应用有 XSS 漏洞,那么本地存储的所有数据就都可能被攻击者的 JS 脚本读取到。如果用户在公用电脑上使用了这个前端应用,那么当用户离开后,这些数据是否也被彻底清除了呢?前端对数据加密后再存储看上去是个防御办法,但其实仅仅提高了一点攻击门槛而已,因为加密所用到的密钥同样存储在前端,有耐心的攻击者依然可以攻破加密这道关卡。
所以,在前端存储敏感、机密信息始终都是一件危险的事情,推荐的做法是尽可能不在前端存这些数据。
8、缺乏静态资源完整性校验
出于性能考虑,前端应用通常会把一些静态资源存放到 CDN(Content Delivery Networks)上面,例如 Javascript 脚本和 Stylesheet 文件。这么做可以显著提高前端应用的访问速度,但与此同时却也隐含了一个新的安全风险。
如果攻击者劫持了 CDN,或者对 CDN 中的资源进行了污染,那么我们的前端应用拿到的就是有问题的 JS 脚本或者 Stylesheet 文件,使得攻击者可以肆意篡改我们的前端页面,对用户实施攻击。这种攻击方式造成的效果和 XSS 跨站脚本攻击有些相似,不过不同点在于攻击者是从 CDN 开始实施的攻击,而传统的 XSS 攻击则是从有用户输入的地方开始下手的。
防御这种攻击的办法是使用浏览器提供的 SRI(Subresource Integrity)功能。顾名思义,这里的 Subresource 指的就是 HTML 页面中通过<script>和<link>元素所指定的资源文件。
每个资源文件都可以有一个 SRI 值,就像下面这样。它由两部分组成,减号(-)左侧是生成 SRI 值用到的哈希算法名,右侧是经过 Base64 编码后的该资源文件的 Hash 值。
<script src=“https://example.js” integrity=“sha384-eivAQsRgJIi2KsTdSnfoEGIRTo25NCAqjNJNZalV63WKX3Y51adIzLT4So1pk5tX”></script>
浏览器在处理这个 script 元素的时候,就会检查对应的 JS 脚本文件的完整性,看其是否和 script 元素中 integrity 属性指定的 SRI 值一致,如果不匹配,浏览器则会中止对这个 JS 脚本的处理。
看到就是学到~是不是还意犹未尽,是不是还想再来几篇干货?点击下方二维码关注深正互联官方公众号,小编每天分享更多更有趣的学习小妙招哦~
深圳 · 龙岗 · 大运软件小镇22栋201
电话:400 182 8580
邮箱:szhulian@qq.com