近几年随着业务的不断发展,前端随之面临很多安全挑战。我们在日常开发中也需要不断预防和修复安全漏洞。接下来,梳理一些场景的前端安全问题和对应的解决方案。
XSS攻击介绍
XSS是后端的责任,后端应该在用户提交数据的接口对隐私敏感的数据进行转义。
NO,这种说法不对
所有插到页面的数据,都要进行过滤转移,当没有敏感字符的时候,就可以直接插到页面上显示了。
NO,丝毫没有什么作用
XSS攻击是页面被注入了恶意的代码,利用恶意脚本,攻击者可以获取用户的Cookie、SessionID等敏感信息。
XSS注入的方法:
- 在HTML中内嵌的文本中,恶意内容通过script标签注入
- 在内联的JS中,拼接的数据突破了原本的限制
- 在标签属性中,恶意内容包含引导,从而突破属性值的限制
- 在标签href、src等属性中,包含
javascript:
可执行代码 - 在onload、onerror、onclick事件中,注入不受控制的代码
- 在style属性中,类似
background- image: url("javascript:..")
代码
如果开发者没有将用户输入的文本进行合适的过滤,直接插入到HTML中,很容易造成注入漏洞。
XSS攻击的分类
存储型XSS
其攻击步骤如下:
- 攻击者将恶意代码提交到目标网站的数据库
- 用户打开目标网站,服务端将恶意代码取出,拼在HTML中返回给浏览器
- 用户浏览器解析执行,混在HTML中的恶意代码也被执行
- 恶意代码窃取用户数据,冒充用户,执行攻击者制定的操作
常见场景:论坛发帖、商品评论、私信
反射型XSS
其攻击步骤如下:
- 攻击者构造特殊URL,其中包含恶意代码
- 用户点击打开该URL,服务端将恶意代码从URL中取出,拼接在HTML中返回给浏览器
- 浏览器解析执行,混在HTML中的恶意代码也被执行
- 恶意代码窃取用户数据,冒充用户,执行攻击者制定的操作
看到这里,我们发现反射型XSS和存储型XSS的区别在于恶意代码的存储位置不同:
- 反射性XSS:恶意代码存URL上
- 存储型XSS:恶意代码存数据库里
所以反射性XSS攻击通常借助URL传递参数,如网站搜索、跳转等。
DOM型XSS
其攻击步骤如下:
- 攻击者构造特殊URL,其中包含恶意代码
- 用户点击打开带有恶意代码的URL
- 用户浏览器解析执行,前端JS取出URL中恶意代码并执行
- 恶意代码窃取用户数据,冒充用户,执行攻击者制定的操作
DOM型XSS和前两种的区别在于:取出和执行恶意代码都在浏览器端执行,属于JS自身的安全漏洞,而存储型XSS、反射型XSS都属于服务端的安全漏洞。
XSS攻击的防御
通过前面的介绍可以发现,XSS攻击两大因素:
- 攻击者提交恶意代码
- 浏览器执行了恶意代码
所以,我们可以在这两个方面进行防御。
我们要考虑是否能够在用户输入的过程中,过滤到恶意代码,然后提交到后端。
这条路是不可行的,一旦攻击者绕过前端过滤,就可以直接构造请求来提交恶意代码了。
如果我们将过滤的时机转移到:在后端写入数据库前,后端进行过过滤,只存储安全的内容,并返回给前端,这样能成功防御XSS吗?
在实际的开发中,我们并不确定内容要输出到哪里,用户输入的内容可能同时提供给前端和客户端,而一旦经过escapeHtml()
,客户端显示的内容就会变成乱码。
所以,这样的方式会引入很大的不确定性和乱码问题,我们通常不这么做。
预防存储型和反射型XSS攻击
这两种攻击都发现在服务端取出恶意代码并拼接在HTML中,最终被浏览器执行,常见的预防方式:
- 改成纯前端渲染,把代码和数据分离开
- 对HTML做转义(采用适合的转义库)
预防DOM型XSS攻击
实际上是网站JS代码本身不够严谨,把不可信数据当作可执行代码。
所以我们在使用.innerHTML
、outerHTML
时要特别注意,不要把不可信数据作为HTML插入页面。
在vue和react中,也不建议使用v-html
和dangerouslySetInnerHTML
检测XSS方法
- 使用CSS攻击字符串手动检测XSS漏洞
- 使用扫描工具检测(ecsypno)
在github小有个工具库:点击直达
我们将它copy 并拼接在URL参数上,就可以检测了:
jaVasCript:/*-/*`/*\`/*'/*"/**/(/* */oNcliCk=alert() )//%0D%0A%0d%0a//</stYle/</titLe/</teXtarEa/</scRipt/--!>\x3csVg/<sVg/oNloAd=alert()//>\x3e
虽然很难通过技术手段完全避免 XSS,但我们可以总结以下原则减少漏洞的产生:
- 利用模板引擎
- 避免内联事件
- 避免拼接 HTML
- 增加攻击难度,降低攻击后果
- 主动检测和发现
CSRF攻击介绍
CSRF利用受害者的注册凭证,绕过后台验证,冒充用户执行某些操作。
典型的流程:
- 受害者登录a.com,并保留了登录cookie
- 攻击者诱导用户访问b.com
- b.com向a.com发送请求
- a.com收到请求后,校验通过,误以为是受害者自己发送的请求
- a.con以受害者名义执行某些操作
- 攻击完成,受害者浑然不知
常见的CSRF攻击类型
GET
<img src="http://bank.example/withdraw?amount=10000&for=hacker" >
在受害者访问含有这个img的页面后,浏览器会自动向http://bank.example/withdraw?account=xiaoming&amount=10000&for=hacker
发出一次HTTP请求。bank.example就会收到包含受害者登录信息的一次跨域请求。
POST
通常CSRF利用一个自动提交的表单,相当于模拟用户完成一次POST操作。
CSRF的特点
- 攻击一般发起在第三方网站,被攻击的网站无法防止攻击
- 利用受害者的登录凭证,冒充受害者,而不是直接窃取数据
- 整个过程不能获取到受害者的登录凭证,仅仅是冒用
- 通常的方式:图片URL、超链接、CORS、From表单提交
CSRF通常是跨域的,所以我们可以专门制定防御策略,比如:
- 组织不明外域的访问:同源检测、Samesite Cookie
- 提交时加上本域才有的信息:CSRF Token、双重Cookie验证
同源检测
在HTTP中每个异步请求都会携带两个Header:Origin Header、Referer Header
服务器可以通过解析这两个Header中的域名,确定请求的来源域
CSRF Token
- 将CSRF Token输出到页面
- 页面提交的请求中携带这个Token
- 服务器验证Tojen是否正确
⚠️注意:Token随机生成,不会被攻击者才到
Samesite Cookie
为Set-Cookie
响应头新增Samesite属性,它用来标明这个 Cookie是个“同站 Cookie”,同站Cookie只能作为第一方Cookie,不能作为第三方Cookie,Samesite 有两个属性值,分别是 Strict 和 Lax:
- Strict:严格模式,任何情况下都不能作为第三方Cookie
- Lax:宽松模式,Cookie可作为第三方Cookie
双重Cookie验证
比CSRF Token繁琐一些,而且不能在通用的拦截上统一处理所有的接口:
- 在用户访问网站时,向请求的域名注入一个cookie,内容为随机字符串
- 前端向后端发起请求时,取出cookie,并添加在URL参数中
- 后端接口验证cookie字段与URL参数中字段是否一致
它的好处在于无需使用Session,适用面广。且Token存在客户端,也不会给服务器造成压力。但这样Cookie中增加了额外的字段。如果有其他漏洞(如XSS),攻击者可以注入cookie,防御就会失效。
为了更好的防御CSRF,最佳实践就是结合上述的防御措施的优缺点来综合考虑。