我正在参加「掘金·启航计划」
前言
前端安全一直是一个老生常谈的问题,你说它重要吧,你说的对!说它不重要吧,涉及安全这两个字就没有不重要的,最重要的是,面试官想知道。
近期,我司就发生了一起网络安全事件。
还记得那是距国庆假期还有3小时21分48秒的时候,我正数着秒,期待着祖国母亲生日的降临。突然,我的工作邮箱闪了一下,啪一下,很快啊,弹出了一封邮件,内容大概是这样的:
邮箱来源为人力资源部,账号以及后缀也和我的一致,看上去挺像那么一回事的。但是这个骗子他不了解我,不了解我们公司,不了解我有多么了解我们公司。作为一名练习时长两年半的老员工,我对公司了解的非常深沉,工资补贴什么的,不能说全等于null也可以说等于了。因此,我断定,这是一封诈骗邮件。很快,公司就发了预警邮件,呼吁大家不要上当,但还是有个别“手速快”的同事扫码填写了个人相关信息,包括银行卡密码。只能说,不愧是你!
根据攻击方式来看,这是一起CSRF攻击事件,那么CSRF是什么呢?结合这次事件,我们对前端常见的安全问题以及解决方案做一下总结。
跨站请求伪造
什么是跨站请求伪造
跨站请求伪造(Cross-site request forgery) ,简称为CSRF,是一种利用用户信息,执行非用户本意操作的攻击方式。与XSS(跨站脚本攻击)不同,XSS 是利用用户对指定网站的信任,CSRF 利用的是网站对用户信息的信任,或者说是对携带用户信息执行操作的信任。
CSRF攻击方式
CSRF的攻击方式很简单,在用户访问正常网站同时访问了第三方网站,第三方网站利用用户正常网站存储的身份标识发出非用户自愿的操作或请求。
举个常见例子,小坤日常使用的聊天软件A收到了一条来自朋友的消息,内容为“澳门赌场、美女荷官”云云,附带一条链接,小坤心里清楚,这很有可能是一个有风险的网站。但是,他还是怀着不可名状的心情,点开了这条链接,点开后又是一条链接加一堆广告,经历了好几次套娃后,小坤的手机卡住了,终于,小坤激动的心冷静了下来,他重启了手机,清除了浏览器缓存,一切似乎什么都没有发生。几天后,小坤的朋友收到了小坤的一条消息,附带一条链接......
小坤的遭遇就是典型的链接CSRF,用户点击即触发,通常嵌套在广告或诱导性内容中。由于小坤登录了A软件,并保持着登录状态主动访问了第三方恶意链接,于是CSRF攻击成功。除了点击链接的形式,CSRF恶意链接还可以嵌在资源URL、form表单、超链接以及CORS(跨域资源共享)中,甚至可以嵌在正常第三方网站的部分内容中,当加载这些内容时,会主动向内容嵌入链接发送一次携带当前用户登录信息的请求,可以说是防不胜防。
CSRF防御
看了小坤的例子我们来总结下CSRF的特点:
-
访问了第三方的恶意链接,通常是跨域的;
-
在保持登录状态访问的,身份标识被冒用;
根据CSRF的特点,我们对其进行针对性防范。
跨域检测
CSRF的恶意链接通常是跨域的,那么我们对跨域的链接进行检测。HTTP中存在这两个请求头:Referer和Origin。Origin和Referer都表示请求的来源,区别是Origin仅包含协议、主机、端口,而Referer还包含其余的PATH参数。浏览器在发起请求时,通常会包含这两种头,仅仅需要我们添加校验,禁用外部来源的请求即可。跨域检测相对简单,但这钟方式因对请求头依赖过高有很多局限性。
Origin标头在以下几种情况不存在:
- 跨域的重定向请求;
- 跨域的媒体文件(图片、音频等)
- 请求协议不是HTTP、HTTPS、WebSocket、Gopher或者是文件URL。
- 跨域重定向。
- iframe的 sandbox 属性设置不为 allow-same-origin 。
- 响应为网络错误。
Referer同样存在不会被携带的限制:
- 来源页面协议为表示本地文件的 URI;
- 当前请求为非安全协议,来源页面为安全协议(HTTPS);
- 因涉及用户隐私,主动设置请求时不携带;
token校验
因为攻击者只是冒用用户的身份标识,并不能获取,因此我们可以在一些关键的请求上添加token并进行验证。
具体流程为:
-
服务端通过用户信息生成token;
//生产token的方式很多,这里我们用jsonwebtoken模块生成 // 引入jsonwebtoken const jwt = require('jsonwebtoken'); //传入用户信息 generateToken(userData) { //时间戳 let created = Math.floor(Date.now() / 1000); //密钥(可根据情况生成,这里简单定义下) let cert = 'zheshimiyao'; let token = jwt.sign({ userData }, cert, { // 过期时间 expiresIn: created + 60 * 60}); return token; }
-
页面请求时携带这个token;
<!--如提交form表单时可添加隐藏input携带token--> <input type=”hidden” name=”tokenValue” value=”token”/>
-
服务端对接受到的token进行校验;
// 校验token verifyToken(token) { return new Promise((resolve, reject) => { //密钥 let cert = 'zheshimiyao'; jwt.verify(token, cert, (err, data) => { if (err) { // token过期或错误 resolve(false) } else { // 验证通过,返回相关数据 resolve(data) } }) })
token校验是一个非常有效的CSRF防御方法,但是对所有页面的表单提交和请求都添加对应的token,在服务端再次进行校验工作量又太大,还容易遗漏。因此,逐个添加token校验是一个有效但不高效的方法。
SameSite
Set-Cookie是一个通过服务器端向用户代理发送cookie的响应头,用户代理可再后续的请求中将其发送回服务器。服务端发送多少个 cookie,就携带多少个Set-Cookie响应头。这个头有一个属性叫SameSite,它允许服务器设定一条cookie不随跨站请求一起发送。
它的使用方法为:
Set-Cookie: cookieName=cookieValue; SameSite=Lax
可选参数有三个:
Strict:任何来自跨域(包括跨协议)的请求,不会携带cookie;
Lax:点击链接跳转时会携带cookie,但通过异步发起的请求、iframe、加载媒体资源等的请求不会携带cookie。
None: 跨站和同站请求中均携带 cookie。但设置None时,必须同时设置 Secure(一个安全属性的标识,只有在使用 https请求时才会发送cookie)。
SameSite=None; Secure
其他
添加验证码进行校验;
重要请求前要求输入密码;
跨站脚本攻击
什么是跨站脚本攻击
跨站脚本攻击 (Cross Site Scripting) ,为了和CSS区分,简称为XSS,是一种代码注入攻击手段。攻击者在正常的网页注入恶意的脚本代码,浏览器无法分辨哪些代码是恶意的脚本代码,当用户浏览器运行网页时,这些恶意脚本也会被浏览器执行,通过这些恶意脚本,攻击者可以盗取用户敏感信息,进行一些非法操作(盗取用户财产、修改企业数据、攻击其他网站、窃取重要商业价值材料等)。
XSS分类
XSS主要分为三类,存储型、反射型、DOM型。
存储型XSS:
存储型XSS,也叫持久型XSS。攻击者通过使用目标网站,将恶意代码提交至目标网站数据库中,用户打开该网站时,服务端会把数据库中的恶意代码取出,返回给浏览器;返回浏览器时,恶意代码会和HTML代码段拼接在一起,被浏览器响应解析,恶意代码被执行。
反射型XSS:
反射型XSS也叫非持久性XSS。攻击者将恶意代码伪装构建出一个URL,用户通过某种操作访问了带有恶意代码的URL,服务端将URL中的恶意代码与HTML拼接,返回给浏览器,浏览器响应解析,恶意代码被执行。
DOM型XSS:
攻击者将恶意代码伪装构建出一个URL,将该URL置于DOM标签或属性内,javaScript取出并执行了URL中的恶意代码。
区别:
存储位置不同:存储型XSS恶意代码存储在数据库中,数据库只要存在,恶意代码一直会存在;反射型XSS和DOM型恶意代码存储在特定URL中,只要不访问、打开该URl,恶意代码就不存在。
攻击对象、范围不同:存储型XSS因为存储在数据库中,只要调用数据库中恶意代码的用户都会被攻击,属于广撒网,范围比较大;反射型XSS和DOM型XSS恶意代码存储在特定URL中,用户只有访问、打开该URL才会被攻击,范围较小。
输入输出场景不同:存储型XSS一般由留言、评论、私信等方式输入恶意代码,通过服务端由数据库取出;反射型XSS一般由搜索、跳转访问URL输入恶意代码,通过服务端从URL取出;DOM型XSS将URL通过DOM节点输入,客户端javaScript取出。
XSS防御
输入过滤、转义
很多XSS攻击的恶意代码都是由前端提交,浏览器执行的,那我我们直接对前端输入内容进行过滤、转义,是否可以避免呢?答案是肯定的,使用如下规则可对一些常用的、易携带恶意代码的字符进行转义。
字符 | 转义后字符 |
---|---|
“ | " |
< | < |
' | ' |
/ | / |
& | & |
` | ` |
还有 > 转义后为 > (不清楚为什么 > 写在表格第一列不会显示。。)
<script>alert('XSS');</script>
<!--携带xss的恶意代码可转义为-->
<script>alert('XSS');</script>
然而,很多时候我们并不能确认我们的输入内容是要去往哪里,可能直达服务端存入数据库,也可能作为富文本显示或作为变量提交,当作为富文本或变量时,输入内容转义就会出现乱码、展示错误等问题。因此,使用转义字符可以避免某些情况下的XSS,但是会产生很多不确定的问题。
属性、方法内避免使用未知来源数据
对DOM型XSS来说,恶意代码的输入途径无非就时标签和属性的内容,只要严格防范非法数据、合理使用标签及相关方法,那么可以很大程度上避免。如:
.innerHTML、location、onclick、href、定时器等等,这些能够运行字符串的标签、属性或方法如果传入内容不谨慎,引入不可信内容,都会产生XSS隐患。
<!--方法内包含恶意代码-->
element.innerHTML = "alert('XSS');";
location.href = "alert('XSS');";
<!--内联事件包含恶意代码-->
<button onclick="alert('XSS');"></button>
<!--属性包含恶意代码-->
<a href="alert('XSS');"></a>
CSP
CSP(Content Security Policy)内容安全策略,用于检测和削弱某些特定类型的攻击,可以有效减少和防范XSS攻击。本质上来说,CSP是一种配置白名单的方式,通过配置确定浏览器可执行脚本的的来源是否被认可,来削弱或消除XSS攻击的依赖载体。CSP配置后的浏览器仅仅会执行白名单域获取到的脚本文件,其他的任何脚本都会被忽略。
CSP的使用
CSP通常在HTTP响应头中配置Content-Security-Policy
,我们只需要配置,浏览器会根据我们的配置实现拦截。CSP允许一个资源指定多个策略,HTTP头也可多次使用。
使用方式为:Content-Security-Policy:指令 内容
内容包括指令的值或者URL,指令值必须带引号。
//connect-src:限制能通过脚本接口的URL
//通过XMLHttpRequest、WebSockt等请求非配置的URL均会被拦截。
Content-Security-Policy: connect-src https://xxx.com/;
//child-src:限制Web Workers(构造函数,构造一个独立主线程外的woeker线程运行脚本)、内嵌浏览器内容(如iframe)等。
Content-Security-Policy: child-src https://xxx.com/;
//备选指令,部分常用指令不存在时,该指令生效,作用由指令值决定。
Content-Security-Policy:default-src https://xxx.com/;
connect-src https://xxx.com/;
除此之外还可以通过<meta>
元素来使用:
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; ">
CSP指令
常用指令及其作用:
指令 | 作用 | |
---|---|---|
child-src | 限制Web Worker和<iframe> (同时限制),分别限制时应使用其他指令 | |
connect-src | 限制通过脚本接口加载的URL | |
default-src | 默认指令,作为其他取指令的备用策略 | |
font-src | 限制通过@font-face加载的字体地址 | |
frame-src | 限制浏览器内嵌内容,如<iframe> | |
img-src | 限制图片和图标地址 | |
media-src | 限制通过<audio>、<video> 等标签加载的媒体文件的源地址 | |
script-src | 限制 JavaScript 的源地址 | |
style-src | 限制css文件源 | |
worker-src | 限制Worker脚本源 | |
plugin-types | 限制可加载资源类型来限制插件 | |
form-action | 限制form的action属性 | |
block-all-mixed-content | 当HTTPS加载页面时,阻止HTTP加载任何资源 | |
sandbox | 为请求资源启用沙盒环境 |
常用值的作用:
指令值 | 作用 |
---|---|
域名或IP地址和端口号 | 限制URL |
协议,必须带引号(‘HTTPS’) | 限制协议,可限制数据协议 |
‘self’ | 不限制同源内容 |
'unsafe-inline' | 不限制内联资源 |
'unsafe-eval' | 不限制 eval() 以及相似的函数根据从字符串创建代码 |
'none' | 限制所有内容 |
虽然CSP可以有效的检测并缓解跨站攻击和内容注入攻击带来的危害,但它的应用率并不高。因为CSP提供强大安全防护的同时,也带来了一些限制:如远程脚本加载、内嵌js代码无法加载、eval()函数的使用等等。对开发者来说,使用CSP就不得不对代码进行分离、调整,进而影响开发效率。因此,CSP的使用需要结合实际的业务场景,需要对症下药。
其他
输入内容校验:如电话、邮箱、密码等敏感信息,要有长度和格式上的校验;
验证码:验证码可以有效防止冒充用户输入;
HTTP-only Cookie: 禁止 JavaScript 读取敏感 Cookie,即使完成恶意代码注入也无法读取Cookie
点击劫持漏洞及防御
什么是点击劫持
点击劫持 (Clickjacking) 也被称为界面伪装攻击,是一种视觉上的欺骗手段。攻击者通过使用 iframe构建透明的页面覆盖着正常的网页上。当用户在进行网页操作时,很容易被诱导,在自身不知情的情况下,已经点击了攻击者事先准备好的恶意链接或按钮上。攻击者可以通过这些操作构建钓鱼网站,盗取用户相关信息,也可以与 XSS 和 CSRF 攻击相结合,突破传统的防御措施,提升漏洞的危害程度。
点击劫持实现原理
以掘金首页为例,我们把掘金的链接放在iframe里,给上层这里新增一个正常页面的输入框,放在与掘金首页搜索框相同的位置。同时设置iframe的css,将z-index数值设置高于正常页面页面的z-index,同时设置其透明度。
当透明度设置为0时,完全看不到掘金首页。
我们在这个输入框输入内容并回车,实际将内容输入到掘金的搜索框内了,因为iframe页面的层级更高。假设这个输入框就是我们使用的网站,再假设掘金是一个钓鱼网站(此处有🐕头),那么一些信息不就输入到钓鱼网站了么,并且是在用户毫不知情的情况下。
点击劫持的防御方法
客户端防御
1、 使用安全、新版的浏览器 正规安全的浏览器通常会提供一些应对点击劫持漏洞的安全机制,时常更新浏览器,可以完善浏览器的安全机制,能有效的防御点击劫持。
2、 NoScript 扩展 firefox的NoScript有一个插件叫ClearClick,这个检测出当前页面存在的点击劫持,并对用户提出告知和警告。
3、添加验证码
在网页关键信息输入前添加验证码校验,不通过验证码校验,无法输入,如页面被隐藏,用户看不到验证码,无法输入,避免了点击劫持带来的危害。
服务端防御
1、X-FRAME-OPTIONS 机制
X-FRAME-OPTIONS是微软提出的一个http头,专门用来防御利用iframe嵌套的点击劫持攻击。并且其他浏览器如chrome、firefox、edge、ie8+都能很好的支持。这个头可以配置两个参数:
DENY // 拒绝任何iframe嵌套加载
SAMEORIGIN // 允许同源域下嵌套加载iframe`
将X-FRAME-OPTIONS设置为DENY后,再次访问,将会提示该链接拒绝打开。
总结
随着互联网的发展,前端涉及网络安全的场景越来越多,各种漏洞互相结合,产生的攻击方式也千奇百怪,很难通过技术手段完全避免。作为一名程序员我们在处理安全问题时不应该局限于某一种安全策略,要结合当前Web应用的实际情况,做出最合适的选择,尽可能高效、安全的解决问题。更重要的是,我们要在开发过程中应时刻保持警惕,输出规范合理的代码,从源头降低安全问题产生的风险。