澳门新浦京娱乐场网站-www.146.net-新浦京娱乐场官网
做最好的网站

澳门新浦京娱乐场网站:前端安全,前端安全粗

打造双剑合璧的 XSS 前端防火墙

2015/09/30 · HTML5 · XSS

原文出处: 林子杰(@Zack__lin)   

作为前端,一直以来都知道HTTP劫持XSS跨站脚本(Cross-site scripting)、CSRF跨站请求伪造(Cross-site request forgery)。但是一直都没有深入研究过,前些日子同事的分享会偶然提及,我也对这一块很感兴趣,便深入研究了一番。

JavaScript 防 http 劫持与 XSS

2016/08/17 · JavaScript · 1 评论 · http劫持, X DNS劫持, XSS, 安全

本文作者: 伯乐在线 - chokcoco 。未经作者许可,禁止转载!
欢迎加入伯乐在线 专栏作者。

作为前端,一直以来都知道HTTP劫持XSS跨站脚本(Cross-site scripting)、CSRF跨站请求伪造(Cross-site request forgery)。但是一直都没有深入研究过,前些日子同事的分享会偶然提及,我也对这一块很感兴趣,便深入研究了一番。

最近用 JavaScript 写了一个组件,可以在前端层面防御部分 HTTP 劫持与 XSS。

当然,防御这些劫持最好的方法还是从后端入手,前端能做的实在太少。而且由于源码的暴露,攻击者很容易绕过我们的防御手段。但是这不代表我们去了解这块的相关知识是没意义的,本文的许多方法,用在其他方面也是大有作用。

已上传到 Github – httphijack.js ,欢迎感兴趣看看顺手点个 star ,本文示例代码,防范方法在组件源码中皆可找到。

接下来进入正文。

1.XSS

参考

  1. 白帽子讲web安全(书)
  2. XSS前端防火墙
  3. JavaScript防http劫持与XSS
  4. 内容安全策略(Content Security Policy,CSP)介绍
  5. 浅谈CSRF攻击方式

前言

深入接触 xss 注入是从排查业务的广告注入开始,以前对 xss 注入片面认为是页面输入的安全校验漏洞导致一系列的问题,通过对 zjcqoo 的《XSS 前端防火墙》系列文章,认识到自己其实对 XSS 注入的认识还真是半桶水。

最近用 JavaScript 写了一个组件,可以在前端层面防御部分 HTTP 劫持与 XSS。

HTTP劫持、DNS劫持与XSS

先简单讲讲什么是 HTTP 劫持与 DNS 劫持。

什么是XSS?

XSS是跨站脚本攻击(Cross-Site Scripting)的简称。
XSS的本质是浏览器错误的将攻击者提供的用户输入数据当做JavaScript脚本给执行。

安全世界观

捣蛋的运营商

由于 xss 注入的范围太广,本文仅对网关劫持这一方面的 XSS 注入进行讨论。
这里读者有个小小的疑问,为什么我要选网关劫持进行讨论?因为网关劫持可以大面积范围进行有效控制。

曾经,有这样一道风靡前端的面试题(当然我也现场笔试过):当你在浏览器地址栏输入一个URL后回车,将会发生的事情?其实本文不关心请求发到服务端的具体过程,但是我关心的时,服务端响应输出的文档,可能会在哪些环节被注入广告?手机、路由器网关、网络代理,还有一级运营商网关等等。所以,无论如何,任何网页都得经过运营商网关,而且最调(zui)皮(da)捣(e)蛋(ji)的,就是通过运营商网关。

另外, 也提醒大家,如果手机安装了一些上网加速软件、网络代理软件或设置网络代理 IP,会有安全风险,也包括公共场所/商家的免费 WIFI。

当然,防御这些劫持最好的方法还是从后端入手,前端能做的实在太少。而且由于源码的暴露,攻击者很容易绕过我们的防御手段。但是这不代表我们去了解这块的相关知识是没意义的,本文的许多方法,用在其他方面也是大有作用。

HTTP劫持

什么是HTTP劫持呢,大多数情况是运营商HTTP劫持,当我们使用HTTP请求请求一个网站页面的时候,网络运营商会在正常的数据流中插入精心设计的网络数据报文,让客户端(通常是浏览器)展示“错误”的数据,通常是一些弹窗,宣传性广告或者直接显示某网站的内容,大家应该都有遇到过。

防御

对输入进行严格的数据编码。
设置CSP HTTP Header、输入验证、开启浏览器XSS防御。

web安全的兴起

web攻击技术经历几个阶段

  1. 服务器端动态脚本的安全问题
  2. sql注入的出现
  3. xss的出现
  4. web攻击思路从服务器到客户端

前端防火墙的实践

经过近一段时间通过对 zjcqoo 的《XSS 前端防火墙》六板斧的反复琢磨理解,基本上防御措施可以归为两大类:一种是从协议上屏蔽,一种是从前端代码层面进行拦截移除。通过 zjcqoo 提出的几种注入防御方式,进行几个月的实践观察,对广告注入方式大概可以归为两种:完全静态注入、先静态注入后动态修改(创建)。

  1. 完全静态注入
    完全内联 js、css、和 dom,不管是 body 内外,甚是恶心,而且如果是在监控脚本前面注入的,还可以抢先执行,造成防御不起作用。注入的 DOM 也无法清除。
  2. 先静态注入后动态修改
    这种可以分为几种:一种是异步请求接口数据再生成 DOM 注入,一种是修改 iframe 源地址进行引入,另外一种是修改 script 源地址,请求执行 js 再异步获取数据或生成 DOM。

已上传到 Github – httphijack.js ,欢迎感兴趣看看顺手点个 star ,本文示例代码,防范方法在组件源码中皆可找到。

DNS劫持

DNS劫持就是通过劫持了DNS服务器,通过某些手段取得某域名的解析记录控制权,进而修改此域名的解析结果,导致对该域名的访问由原IP地址转入到修改后的指定IP,其结果就是对特定的网址不能访问或访问的是假网址,从而实现窃取资料或者破坏原有正常服务的目的。

DNS 劫持就更过分了,简单说就是我们请求的是  ,直接被重定向了 ,本文不会过多讨论这种情况。

参考文章

前端防御从入门到弃坑--CSP变迁
CSP Is Dead, Long Live CSP! 翻译

安全三要素

机密性confidentiality、完整性integrity、可用性availability

监控数据观察分析

对 zjcqoo 提出的几种防御方式的实践,前一个月主要是花在优化检测脚本和增加白名单过滤脏数据方面,因为这块事情只能利用业余时间来搞,所以拖的时间有点久。白名单这块的确是比较繁琐,很多人以为分析下已知的域名就 ok 了,其实不然,云龙在这篇 iframe 黑魔法就提到移动端 Native 与 web 的通信机制,所以在各种 APP 上,会有各种 iframe 的注入,而且是各种五花八门的协议地址,也包括 chrome。

监控拿到的数据很多,但是,由于对整个广告注入黑产行业的不熟悉,所以,有必要借助 google 进行查找研究,发现,运营商大大地狡猾,他们自己只会注入自己业务的广告,如 4G 免费换卡/送流量/送话费,但是商业广告这块蛋糕他们会拱手让人?答案是不可能,他们会勾结其他广告代理公司,利用他们的广告分发平台(运营商被美名为广告系统平台提供商)进行广告投放然后分成…

对于用户投诉,他们一般都是认错,然后对这个用户加白名单,但是他们对其他用户还是继续作恶。对于企业方面的投诉,如果影响到他们的域名,如果你没有确凿的证据,他们就会用各种借口摆脱自己的责任,如用户手机中毒等等,如果你有确凿的证据,还得是他们运营商自己的域名或者 IP,否则他们也无法处理。他们还是一样的借口,用户手机中毒等等。

除非你把运营商的域名或 IP 监控数据列给他看,他才转变态度认错,但是这仅仅也是之前我们提到的流量话费广告,对于第三方广告代理商的广告,还是没法解决,这些第三方广告代理商有广告家、花生米、XX 传媒等等中小型广告商,当然也不排除,有的是“个体户广告商”。

从另一方面来看,由于使用的是古老的 http 协议,这种明文传输的协议,html 内容可以被运营商一清二楚地记录下来,页面关键字、访问时间、地域等用户标签都可以进行采集,说到这,你可能已经明白了一个事(隐私侵犯已经见怪不怪了)——大数据分析 个性化推荐,在 google 一查,运营商还真有部署类似于 iPush 网络广告定向直投这样的系统,而且广告点击率也出奇的高,不排除会定向推送一些偏黄色的图片或游戏。

另外,数据分析中发现一些百度统计的接口请求,也在一些 js 样本中发现百度统计地址,猜测很有可能是这种广告平台利用百度统计系统做数据分析,如定向投放用户 PV 统计,广告效果统计等等。
监控数据分析也扯这么多了,我们还是回来看怎么做防御措施吧!

接下来进入正文。

XSS跨站脚本

XSS指的是攻击者漏洞,向 Web 页面中注入恶意代码,当用户浏览该页之时,注入的代码会被执行,从而达到攻击的特殊目的。

关于这些攻击如何生成,攻击者如何注入恶意代码到页面中本文不做讨论,只要知道如 HTTP 劫持 和 XSS 最终都是恶意代码在客户端,通常也就是用户浏览器端执行,本文将讨论的就是假设注入已经存在,如何利用 Javascript 进行行之有效的前端防护。

2.iframe

设计安全方案的原则

  • secure by default原则:白名单
  • 纵深原则:不同层面实施安全方案,避免疏漏; 正确的地方做正确的事
  • 数据与代码分离原则(针对各种注入问题)
  • 不可预测性原则:敏感数据不可预测

防御措施介绍

 

页面被嵌入 iframe 中,重定向 iframe

先来说说我们的页面被嵌入了 iframe 的情况。也就是,网络运营商为了尽可能地减少植入广告对原有网站页面的影响,通常会通过把原有网站页面放置到一个和原页面相同大小的 iframe 里面去,那么就可以通过这个 iframe 来隔离广告代码对原有页面的影响。
澳门新浦京娱乐场网站 1

这种情况还比较好处理,我们只需要知道我们的页面是否被嵌套在 iframe 中,如果是,则重定向外层页面到我们的正常页面即可。

那么有没有方法知道我们的页面当前存在于 iframe 中呢?有的,就是 window.self 与 window.top 。

什么是iframe?

iframe是一个容器。该容器可以把另一个HTML页面嵌入到当前页面。
iframe中的内容是由第三方来提供的,默认情况下他们不受我们的控制,他们可以在iframe中运行JavaScirpt脚本、Flash插件、弹出对话框等等,这可能会破坏前端用户体验。
iframe中的域名因为过期而被恶意攻击者抢注,iframe中的内容被替换掉了,从而利用用户浏览器中的安全漏洞下载安装木马、恶意勒索软件。

客户端安全

全站 HTTPS HSTS

开启 HTTPS,可以加强数据保密性、完整性、和身份校验,而 HSTS (全称 HTTP Strict Transport Security)可以保证浏览器在很长时间里都会只用 HTTPS 访问站点,这是该防御方式的优点。但是,缺点和缺陷也不可忽略。

互联网全站HTTPS的时代已经到来 一文已有详细的分析,加密解密的性能损耗在服务端的损耗和网络交互的损耗,但是移动端浏览器和 webview 的兼容性支持却是个问题,比如 Android webview 需要固件4.4以上才支持,iOS safari 8 以上也才支持,而 UC 浏览器目前还不支持。

而目前推动团队所有业务支持 HTTPS 难度也是相当高,部分 302 重定向也有可能存在 SSLStrip,更何况 UC 浏览器还不支持这个协议,很容易通过 SSLStrip 进行劫持利用,虽然运营商大部分情况下不会这么干,但是我还是坚定怀疑他们的节操。由于我国宽带网络的基本国情,短时间指望速度提升基本上不可能的,就算总理一句话,但哪个运营商不想赚钱?所以,业务性能的下降和业务安全,需要进行权衡利弊。

HTTP劫持、DNS劫持与XSS

先简单讲讲什么是 HTTP 劫持与 DNS 劫持。

window.self

返回一个指向当前 window 对象的引用。

防御

使用iframe的sandbox的安全属性。

浏览器安全功能

  • 同源策略Same Origin Policy(SOP)
    限制来自不同源的脚本或document对当前document读取或设置某些属性。
    浏览器中script、img、iframe等标签可以通过src属性跨域加载资源,不受同源策略的限制,对于src加载的资源,浏览器限制JavaScript不能读写。
    XMLHttpRequest原本也根据同源策略限制跨域请求,因此后来W3C制定了新的请求:假设从http://www.a.com/test.html发起一个跨域的XMLHttpRequest请求到http://www.b.com/test.php,发起的请求HTTP投必须带上Origin,而B站点服务器返回一个HTTP头包含Access-Control-Allow-Origin: http://www.a.com,那么这个请求就会被通过

    澳门新浦京娱乐场网站 2

    跨域请求的访问过程

  • 浏览器沙盒
    浏览器发展出多进程架构,将各个功能模块分开,各个浏览器实例分开,提升了安全性。
    Chrome是第一个采用多进程架构的浏览器,主要进程分为:浏览器进程、渲染进程、插件进程、扩展进程。

![](https://upload-images.jianshu.io/upload_images/1802689-abdaec538e57d511.png)

Chrome的架构



渲染引擎由沙盒隔离,
网页代码要与浏览器内核进程、操作系统通信,需要通过IPC
channel,在其中会进行一些安全检查。这可以让不受信任的网页或JavaScript代码运行在一个受限的环境中,保护本地系统的安全。  
Chrome每个标签页和扩展都在独立的沙盒内运行,在提高安全性的同时,一个标签页面的崩溃也不会导致其他标签页面被关闭,但由于过于占用内存,现在已经变成有些网页公用一个进程,它们和服务器保持共同的会话。
  • 恶意网站拦截
    浏览器周期性从从服务器获取恶意网站的黑名单,如果用户访问就弹出警告框

  • Content Security Policy(CSP)
    Firefox4推出Content Security Policy(CSP),后来被其他浏览器支持。
    CSP的做法是,由服务器端返回一个Content-Security-Policy的HTTP头,在其中描述页面应该遵守的安全策略,让浏览器不再盲目信任服务器发送的所有内容,并且能让浏览器只执行或者渲染来自这些源的内容。
    源的策略包括:

    • script-src控制了页面的脚本权限集合
    • connect-src限制了可以连接到的源(通过XHR、WebSockets和EventSource)
    • font-src指定了可以提供web字体的源
    • frame-src列出了可以作为页面帧嵌入的源
    • img-src定义了可以加载图片的源
    • media-src限制了允许发送视频和音频的源
    • object-src允许控制Flash和其他插件
    • style-src控制样式表的源

    源列表接受4个关键词:

    • none,不匹配任何内容
    • self,值匹配当前源,不匹配其子域
    • unsafe-inline,允许内联的JavaScript和CSS
    • unsafe-eval,允许eval这样的文本到JavaScript的机制

    例如:Content-Security-Policy: default-src https://cdn.example.net; frame-src ‘none’;如果想要从一个内容分发网络加载所有资源,而且已知不需要帧内容

    由于CSP配置规则比较复杂,在页面较多的情况下很难一个个配置,后期维护成本大,导致CSP没有很好的推广。

Content Security Policy(简称 CSP)

CSP 内容安全策略,属于一种浏览器安全策略,以可信白名单作机制,来限制网站中是否可以包含某来源内容。兼容性支持同样是个问题,比如 Android webview 需要固件4.4以上才支持,iOS safari 6 以上支持,幸运的是 UC 浏览器目前支持 1.0 策略版本,具体可以到 CANIUSE 了解。目前对 CSP 的使用仅有不到两周的经验而已,下面简单说说其优缺点。

缺点:

  1. CSP 规范也比较累赘,每种类型需要重新配置一份,默认配置不能继承,只能替换,这样会导致整个 header 内容会大大增加。
  2. 如果业务中有爬虫是抓取了外部图片的话,那么 img 配置要么需要枚举各种域名,要么就信任所有域名。
    1. 移动端 web app 页面,如果有存在 Native 与 web 的通信,那么 iframe 配置只能信任所有域名和协议了。
    1. 一些业务场景导致无法排除内联 script 的情况,所以只能开启 unsafe-inline
    1. 一些库仍在使用 eval,所以避免误伤,也只能开启 unsafe-eval
    1. 由于 iframe 信任所有域名和协议,而 unsafe-inline 开启,使得整个防御效果大大降低

优点:

  1. 通过 connect/script 配置,我们可以控制哪些 外部域名异步请求可以发出,这无疑是大大的福音,即使内联 script 被注入,异步请求仍然发不出,这样一来,除非攻击者把所有的 js 都内联进来,否则注入的功能也运行不了,也无法统计效果如何。
  2. 通过 reportUri 可以统计到攻击类型和 PV,只不过这个接口的设计不能自定义,上报的内容大部分都是鸡肋。
  3. object/media 配置可以屏蔽一些外部多媒体的加载,不过这对于视频播放类的业务,也会误伤到。
  4. 目前 UC 浏览器 Android 版本的客户端和 web 端通信机制都是采用标准的 addJavascriptInterface 注入方式,而 iPhone 版本已将 iframe 通信方式改成 ajax 方式(与页面同域,10.5 全部改造完成),如果是只依赖 UC 浏览器的业务,可以大胆放心使用,如果是需要依赖于第三方平台,建议先开启 reportOnly,将一些本地协议加入白名单,再完全开启防御。

总的来说吧,单靠 CSP 单打独斗显然是不行,即使完全开启所有策略,也不能完成消除注入攻击,但是作为纵深防御体系中的一道封锁防线,价值也是相当有用的。

HTTP劫持

什么是HTTP劫持呢,大多数情况是运营商HTTP劫持,当我们使用HTTP请求请求一个网站页面的时候,网络运营商会在正常的数据流中插入精心设计的网络数据报文,让客户端(通常是浏览器)展示“错误”的数据,通常是一些弹窗,宣传性广告或者直接显示某网站的内容,大家应该都有遇到过。

window.top

返回窗口体系中的最顶层窗口的引用。

对于非同源的域名,iframe 子页面无法通过 parent.location 或者 top.location 拿到具体的页面地址,但是可以写入 top.location ,也就是可以控制父页面的跳转。

两个属性分别可以又简写为 self 与 top,所以当发现我们的页面被嵌套在 iframe 时,可以重定向父级页面:

JavaScript

if (self != top) { // 我们的正常页面 var url = location.href; // 父级页面重定向 top.location = url; }

1
2
3
4
5
6
if (self != top) {
  // 我们的正常页面
  var url = location.href;
  // 父级页面重定向
  top.location = url;
}

 

参考文章

HTML 元素参考<iframe>

XSS

跨站脚本攻击,Cross Site Script为了和CSS区分所以叫XSS。
XSS攻击指,攻击者往Web页面里插入恶意html代码,当其它用户浏览该页之时,嵌入其中Web里面的html代码会被执行,从而达到恶意攻击用户的目的。

XSS根据效果可以分成:

  • 反射型XSS:简单把用户输入的数据反射给浏览器,例如诱使用户点击个恶意链接来达到攻击的目的
  • 存储型XSS:把用户输入的数据存储到服务器,例如黑客发表包含恶意js代码的文章,发表后所有浏览文章的用户都会在他们的浏览器执行这段恶意代码

案例:
2011年,新浪微博XSS蠕虫事件:攻击者利用广场的一个反射性XSS URL,自动发送微博、私信,私信内容又带有该XSS URL,导致病毒式传播。百度空间、twitter等SNS网站都发生过类似事件。

前端防火墙拦截

前端防火墙显然适合作为第一道防线进行设计,可以预先对一些注入的内联 js 代码、script/iframe 源引用进行移除,同时对 script/iframe 源地址修改做监控移除。
基本设计逻辑大概如下:

澳门新浦京娱乐场网站 3

详细的实现逻辑,参考zjcqoo 的《XSS 前端防火墙》系列文章。

缺点:

  1. 如果是在监控脚本执行前,注入的脚本已经执行,显然后知后觉无法起防御作用了。
  2. 一些 DOM 的注入显然无能为力。

优点:

  1. 可以针对 iframe 做一些自定义的过滤规则,防止对本地通信误伤。
  2. 可以收集到一些注入行为数据进行分析。

DNS劫持

DNS 劫持就是通过劫持了 DNS 服务器,通过某些手段取得某域名的解析记录控制权,进而修改此域名的解析结果,导致对该域名的访问由原IP地址转入到修改后的指定IP,其结果就是对特定的网址不能访问或访问的是假网址,从而实现窃取资料或者破坏原有正常服务的目的。

DNS 劫持比之 HTTP 劫持 更加过分,简单说就是我们请求的是  ,直接被重定向了 ,本文不会过多讨论这种情况。

使用白名单放行正常 iframe 嵌套

当然很多时候,也许运营需要,我们的页面会被以各种方式推广,也有可能是正常业务需要被嵌套在 iframe 中,这个时候我们需要一个白名单或者黑名单,当我们的页面被嵌套在 iframe 中且父级页面域名存在白名单中,则不做重定向操作。

上面也说了,使用 top.location.href 是没办法拿到父级页面的 URL 的,这时候,需要使用document.referrer

通过 document.referrer 可以拿到跨域 iframe 父页面的URL。

JavaScript

// 建立白名单 var whiteList = [ 'www.aaa.com', 'res.bbb.com' ]; if (self != top) { var // 使用 document.referrer 可以拿到跨域 iframe 父页面的 URL parentUrl = document.referrer, length = whiteList.length, i = 0; for(; i<length; i ){ // 建立白名单正则 var reg = new RegExp(whiteList[i],'i'); // 存在白名单中,放行 if(reg.test(parentUrl)){ return; } } // 我们的正常页面 var url = location.href; // 父级页面重定向 top.location = url; }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 建立白名单
var whiteList = [
  'www.aaa.com',
  'res.bbb.com'
];
if (self != top) {
  var
    // 使用 document.referrer 可以拿到跨域 iframe 父页面的 URL
    parentUrl = document.referrer,
    length = whiteList.length,
    i = 0;
  for(; i<length; i ){
    // 建立白名单正则
    var reg = new RegExp(whiteList[i],'i');
    // 存在白名单中,放行
    if(reg.test(parentUrl)){
      return;
    }
  }
  // 我们的正常页面
  var url = location.href;
  // 父级页面重定向
  top.location = url;
}

 

3.点击劫持

被动扫描 vs 主动防御

  • 被动扫描:把页面里所有元素都扫描一遍,看是否有有危险性的代码;但由于现在ajax的使用,经常会动态修改DOM元素,即使定期扫描,XSS也可以在定时器的间隔触发后销毁,没用且浪费性能。
  • 主动防御:只要防御程序在其他代码之前运行,就可以对XSS攻击主动进行检测和拦截。

双剑合璧

即使是单纯的 DOM 注入,显然无法满足更高级功能的使用,也会使运营商的广告分发平台效果大打折扣。如果单独其中一种方式进行使用,也只是发挥了一招一式的半成功力,如果是双手互搏,那也可以发挥成倍的功力。

而前端防火墙再加上 CSP 安全策略,双剑合璧,则可以大大降低广告注入带来的负面效果,重则造成广告代码严重瘫痪无法运行:在监控脚本后注入广告脚本,基本上可以被前端防火墙封杀殆尽,即使有漏网之鱼,也会被 CSP 进行追杀,不死也残。

即使在监控脚本运行前注入,通过 CSP content-src 策略,可以拦截白名单域名列表外的接口请求,使得广告代码的异步请求能力被封杀,script-src 策略,也可以封杀脚本外链的一些外部请求,进一步封杀异步脚本引用,frame-src 策略无论先后创建的 iframe,一律照杀。

侥幸者躲过了初一,却躲不过十五,前端防火墙拍马赶到,照样封杀无误,唯一的路径只有注入 DOM 这一方式,别忘了,只要开启 img-src 策略配置,广告代码只剩下文字链。虽然是一个文字链广告,但点击率又能高到哪去呢?

如果你是 node 派系,小弟附上《辟邪剑谱》 helmet 一本,如果你的业务有涉及到 UCBrowser,更有《辟邪剑谱之 UC 版》helmet-csp-uc 。

所谓道高一尺魔高一丈,既然我们有高效的防御措施,相信他们不久也会探索出反防御方式,如此,我们也需要和这帮人斗智斗勇,一直等到 HTTP/2 规范的正式落地。

1 赞 3 收藏 评论

澳门新浦京娱乐场网站 4

XSS跨站脚本

XSS指的是攻击者利用漏洞,向 Web 页面中注入恶意代码,当用户浏览该页之时,注入的代码会被执行,从而达到攻击的特殊目的。

关于这些攻击如何生成,攻击者如何注入恶意代码到页面中本文不做讨论,只要知道如 HTTP 劫持 和 XSS 最终都是恶意代码在客户端,通常也就是用户浏览器端执行,本文将讨论的就是假设注入已经存在,如何利用 Javascript 进行行之有效的前端防护。

更改 URL 参数绕过运营商标记

这样就完了吗?没有,我们虽然重定向了父页面,但是在重定向的过程中,既然第一次可以嵌套,那么这一次重定向的过程中页面也许又被 iframe 嵌套了,真尼玛蛋疼。

当然运营商这种劫持通常也是有迹可循,最常规的手段是在页面 URL 中设置一个参数,例如  ,其中 iframe_hijack_redirected=1 表示页面已经被劫持过了,就不再嵌套 iframe 了。所以根据这个特性,我们可以改写我们的 URL ,使之看上去已经被劫持了:

JavaScript

var flag = 'iframe_hijack_redirected'; // 当前页面存在于一个 iframe 中 // 此处需要建立一个白名单匹配规则,白名单默认放行 if (self != top) { var // 使用 document.referrer 可以拿到跨域 iframe 父页面的 URL parentUrl = document.referrer, length = whiteList.length, i = 0; for(; i<length; i ){ // 建立白名单正则 var reg = new RegExp(whiteList[i],'i'); // 存在白名单中,放行 if(reg.test(parentUrl)){ return; } } var url = location.href; var parts = url.split('#'); if (location.search) { parts[0] = '&' flag '=1'; } else { parts[0] = '?' flag '=1'; } try { console.log('页面被嵌入iframe中:', url); top.location.href = parts.join('#'); } catch (e) {} }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
var flag = 'iframe_hijack_redirected';
// 当前页面存在于一个 iframe 中
// 此处需要建立一个白名单匹配规则,白名单默认放行
if (self != top) {
  var
    // 使用 document.referrer 可以拿到跨域 iframe 父页面的 URL
    parentUrl = document.referrer,
    length = whiteList.length,
    i = 0;
  for(; i<length; i ){
    // 建立白名单正则
    var reg = new RegExp(whiteList[i],'i');
    // 存在白名单中,放行
    if(reg.test(parentUrl)){
      return;
    }
  }
  var url = location.href;
  var parts = url.split('#');
  if (location.search) {
    parts[0] = '&' flag '=1';
  } else {
    parts[0] = '?' flag '=1';
  }
  try {
    console.log('页面被嵌入iframe中:', url);
    top.location.href = parts.join('#');
  } catch (e) {}
}

当然,如果这个参数一改,防嵌套的代码就失效了。所以我们还需要建立一个上报系统,当发现页面被嵌套时,发送一个拦截上报,即便重定向失败,也可以知道页面嵌入 iframe 中的 URL,根据分析这些 URL ,不断增强我们的防护手段,这个后文会提及。

什么是点击劫持?

通过iframe引用第三方内容,制作一个透明层到上面,引导到自己的页面。

内联事件

例如在页面中需要用户输入图片的地址如<img src="{路径}" />,但攻击者们可以通过引号提前关闭属性,并添加一个极易触发的内联事件如<img src="{路径" onload="alert('xss')}" />

 

内联事件及内联脚本拦截

在 XSS 中,其实可以注入脚本的方式非常的多,尤其是 HTML5 出来之后,一不留神,许多的新标签都可以用于注入可执行脚本。

列出一些比较常见的注入方式:

  1. <a href="javascript:alert(1)" ></a>
  2. <iframe src="javascript:alert(1)" />
  3. <img src='x' onerror="alert(1)" />
  4. <video src='x' onerror="alert(1)" ></video>
  5. <div onclick="alert(1)" onmouseover="alert(2)" ><div>

除去一些未列出来的非常少见生僻的注入方式,大部分都是 javascript:... 及内联事件 on*

我们假设注入已经发生,那么有没有办法拦截这些内联事件与内联脚本的执行呢?

对于上面列出的 (1) (5) ,这种需要用户点击或者执行某种事件之后才执行的脚本,我们是有办法进行防御的。

防御

使用X-Frame-Options:DENY这个HTTP Header来阻止别人iframe引用自家页面。

防范思路

对于内联事件,还是遵循DOM事件模型:”捕获阶段->目标阶段->冒泡阶段“,如下图。

澳门新浦京娱乐场网站 5

DOM事件模型

因此我们可以在捕获阶段进行检测,拦截目标阶段的事件的执行。

document.addEventListener('click', function(e) {
  var element = e.target; 
  var code = element.getAttribute('onclick');
  if (/xss/.test(code)) { // 拦截的策略判断
    element.onclick = null; // 拦截内联事件,不影响冒泡
    alert('拦截可疑事件: '   code); 
  } 
}, true);

除了onclick事件,还有其他很多内联事件如onload、onerror等,不同浏览器支持的也不一样,可以通过遍历document对象,来获取所有的内联事件名。

for(var item in document) {
  if (/^on./.test(item)) { // 检测所有on*事件
    document.addEventListener(item.substr(2), function(e) { // 添加监听需要去掉on
    // ... 拦截策略等
    }
  }
}

除了on开头的事件外,还有一些特殊形式,其中<a href="javascript:"></a>使用最为广泛和常见,这种就需要单独对待。

document.addEventListener(eventName.substr(2), function(e) {
  //... 其他拦截策略
  var element = e.target; 
  // 扫描 <a href="javascript:"> 的脚本 
  if (element.tagName == 'A' && element.protocol == 'javascript:') {
  // ...
  }
});

对于一些常用的事件如鼠标移动会非常频繁的调用,因此有必要考虑性能方面的优化。
一般来说内联事件在代码运行过程中并不会改变,因此对某个元素的特定事件,扫描一次后置个标志位,之后再次执行的话检测标志位后可以考虑是否直接跳过。

页面被嵌入 iframe 中,重定向 iframe

先来说说我们的页面被嵌入了 iframe 的情况。也就是,网络运营商为了尽可能地减少植入广告对原有网站页面的影响,通常会通过把原有网站页面放置到一个和原页面相同大小的 iframe 里面去,那么就可以通过这个 iframe 来隔离广告代码对原有页面的影响。
澳门新浦京娱乐场网站 6

这种情况还比较好处理,我们只需要知道我们的页面是否被嵌套在 iframe 中,如果是,则重定向外层页面到我们的正常页面即可。

那么有没有方法知道我们的页面当前存在于 iframe 中呢?有的,就是 window.self 与 window.top 。

浏览器事件模型

这里说能够拦截,涉及到了事件模型相关的原理。

我们都知道,标准浏览器事件模型存在三个阶段:

  • 捕获阶段
  • 目标阶段
  • 冒泡阶段

对于一个这样 <a href="javascript:alert(222)" ></a> 的 a 标签而言,真正触发元素 alert(222) 是处于点击事件的目标阶段。

See the Pen EyrjkG by Chokcoco (@Chokcoco) on CodePen.

点击上面的 click me ,先弹出 111 ,后弹出 222。

那么,我们只需要在点击事件模型的捕获阶段对标签内 javascript:... 的内容建立关键字黑名单,进行过滤审查,就可以做到我们想要的拦截效果。

对于 on* 类内联事件也是同理,只是对于这类事件太多,我们没办法手动枚举,可以利用代码自动枚举,完成对内联事件及内联脚本的拦截。

以拦截 a 标签内的 href="javascript:... 为例,我们可以这样写:

JavaScript

// 建立关键词黑名单 var keywordBlackList = [ 'xss', 'BAIDU_SSP__wrapper', 'BAIDU_DSPUI_FLOWBAR' ]; document.addEventListener('click', function(e) { var code = ""; // 扫描 <a href="javascript:"> 的脚本 if (elem.tagName == 'A' && elem.protocol == 'javascript:') { var code = elem.href.substr(11); if (blackListMatch(keywordBlackList, code)) { // 注销代码 elem.href = 'javascript:void(0)'; console.log('拦截可疑事件:' code); } } }, true); /** * [黑名单匹配] * @param {[Array]} blackList [黑名单] * @param {[String]} value [需要验证的字符串] * @return {[Boolean]} [false -- 验证不通过,true -- 验证通过] */ function blackListMatch(blackList, value) { var length = blackList.length, i = 0; for (; i < length; i ) { // 建立黑名单正则 var reg = new RegExp(whiteList[i], 'i'); // 存在黑名单中,拦截 if (reg.test(value)) { return true; } } return false; }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// 建立关键词黑名单
var keywordBlackList = [
  'xss',
  'BAIDU_SSP__wrapper',
  'BAIDU_DSPUI_FLOWBAR'
];
  
document.addEventListener('click', function(e) {
  var code = "";
  // 扫描 <a href="javascript:"> 的脚本
  if (elem.tagName == 'A' && elem.protocol == 'javascript:') {
    var code = elem.href.substr(11);
    if (blackListMatch(keywordBlackList, code)) {
      // 注销代码
      elem.href = 'javascript:void(0)';
      console.log('拦截可疑事件:' code);
    }
  }
}, true);
/**
* [黑名单匹配]
* @param  {[Array]} blackList [黑名单]
* @param  {[String]} value    [需要验证的字符串]
* @return {[Boolean]}         [false -- 验证不通过,true -- 验证通过]
*/
function blackListMatch(blackList, value) {
  var length = blackList.length,
    i = 0;
  for (; i < length; i ) {
    // 建立黑名单正则
    var reg = new RegExp(whiteList[i], 'i');
    // 存在黑名单中,拦截
    if (reg.test(value)) {
      return true;
    }
  }
  return false;
}

可以戳我查看DEMO。(打开页面后打开控制台查看 console.log)

点击图中这几个按钮,可以看到如下:

澳门新浦京娱乐场网站 7

这里我们用到了黑名单匹配,下文还会细说。

 

参考文章

X-Frame-Options防止网页放在iframe中
X-Frame-Options 响应头

可疑模块

XSS最简单和常见的方法就是动态加载个站外的脚本,模拟代码如下:

<button id="btn">创建脚本</button>
<script> 
btn.onclick = function() {
  var el = document.createElement('script'); 
  el.src = 'http://www.etherdream.com/xss/out.js'; 
  // 也可以写成el.setAttriute('src','http://www.etherdream.com/xss/out.js');
  document.body.appendChild(el); 
};
</script>

window.self

返回一个指向当前 window 对象的引用。

静态脚本拦截

XSS 跨站脚本的精髓不在于“跨站”,在于“脚本”。

通常而言,攻击者或者运营商会向页面中注入一个<script>脚本,具体操作都在脚本中实现,这种劫持方式只需要注入一次,有改动的话不需要每次都重新注入。

我们假定现在页面上被注入了一个 <script src="http://attack.com/xss.js"> 脚本,我们的目标就是拦截这个脚本的执行。

听起来很困难啊,什么意思呢。就是在脚本执行前发现这个可疑脚本,并且销毁它使之不能执行内部代码。

所以我们需要用到一些高级 API ,能够在页面加载时对生成的节点进行检测。

 

4.浏览器的容错能力

防范思路

在HTML5中MutationEvent的DOMNodeInserted事件和DOM4提供的MutationObserver接口都可以检测插入的DOM元素。

var observer = new MutationObserver(function(mutations) {
  console.log('MutationObserver:', mutations); 
}); 
observer.observe(document, {
  subtree: true, 
  childList: true 
});
document.addEventListener('DOMNodeInserted', function(e) {
  console.log('DOMNodeInserted:', e); 
}, true);

MutationObserver能捕捉到在它之后页面加载的静态元素,但它不是每次有新元素时调用,而是一次性传一段时间内的所有元素。
而DOMNodeInserted不关心静态元素,但能捕捉动态添加的元素,而且是在MutationObserver之前调用。
对于静态脚本,可以通过MutationObserver来检测和拦截,但对不同的浏览器拦截结果不同,在Firefox上还是会执行。
对于动态脚本,DOMNodeInserted的优先级比MutationObserver高,但也只能检测却无法拦截脚本的执行。

既然无法通过监测DOM元素挂载来拦截动态脚本执行,那么讲检测手段提前,对于动态创建脚本,赋予src属性必不可少,因此我们可以通过监测属性赋值来进行拦截。
检测属性赋值可以通过MutationObserver或DOMAttrModified事件,但对于先赋值再插入元素的情况来说,由于赋值时元素还没插入,因此事件回调并不会被调用。
除了事件外还可以通过重写Setter访问器,在修改属性时触发函数调用。

var raw_setter = HTMLScriptElement.prototype.__lookupSetter__('src');
HTMLScriptElement.prototype.__defineSetter__('src', function(url) {
  if (/xss/.test(url)) { 
    return;
  }
  raw_setter.call(this, url);
});

对于setAttribute来修改属性的情况同样需要一定的防护,通过改写setAttribute。

// 保存原有接口
var old_setAttribute = window.Element.prototype.setAttribute;

// 重写 setAttribute 接口
window.Element.prototype.setAttribute = function(name, value) {

  // 匹配到 <script src='xxx' > 类型
  if (this.tagName == 'SCRIPT' && /^src$/i.test(name)) {
    // 拦截策略
    if (/xss/.test(value)) {
      console.log('拦截可疑setAttribute:', value);
      report('拦截可疑setAttribute', value);
      return;
    }
  }     
  // 调用原始接口
  old_setAttribute.apply(this, arguments);
};

window.top

返回窗口体系中的最顶层窗口的引用。

对于非同源的域名,iframe 子页面无法通过 parent.location 或者 top.location 拿到具体的页面地址,但是可以写入 top.location ,也就是可以控制父页面的跳转。

两个属性分别可以又简写为 self 与 top,所以当发现我们的页面被嵌套在 iframe 时,可以重定向父级页面:

if (self != top) {
  // 我们的正常页面
  var url = location.href;
  // 父级页面重定向
  top.location = url;
}

  

MutationObserver

MutationObserver 是 HTML5 新增的 API,功能很强大,给开发者们提供了一种能在某个范围内的 DOM 树发生变化时作出适当反应的能力。

说的很玄乎,大概的意思就是能够监测到页面 DOM 树的变换,并作出反应。

MutationObserver() 该构造函数用来实例化一个新的Mutation观察者对象。

JavaScript

MutationObserver( function callback );

1
2
3
MutationObserver(
  function callback
);

目瞪狗呆,这一大段又是啥?意思就是 MutationObserver 在观测时并非发现一个新元素就立即回调,而是将一个时间片段里出现的所有元素,一起传过来。所以在回调中我们需要进行批量处理。而且,其中的 callback 会在指定的 DOM 节点(目标节点)发生变化时被调用。在调用时,观察者对象会传给该函数两个参数,第一个参数是个包含了若干个 MutationRecord 对象的数组,第二个参数则是这个观察者对象本身。

所以,使用 MutationObserver ,我们可以对页面加载的每个静态脚本文件,进行监控:

JavaScript

// MutationObserver 的不同兼容性写法 var MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver; // 该构造函数用来实例化一个新的 Mutation 观察者对象 // Mutation 观察者对象能监听在某个范围内的 DOM 树变化 var observer = new MutationObserver(function(mutations) { mutations.forEach(function(mutation) { // 返回被添加的节点,或者为null. var nodes = mutation.addedNodes; for (var i = 0; i < nodes.length; i ) { var node = nodes[i]; if (/xss/i.test(node.src))) { try { node.parentNode.removeChild(node); console.log('拦截可疑静态脚本:', node.src); } catch (e) {} } } }); }); // 传入目标节点和观察选项 // 如果 target 为 document 或者 document.documentElement // 则当前文档中所有的节点添加与删除操作都会被观察到 observer.observe(document, { subtree: true, childList: true });

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// MutationObserver 的不同兼容性写法
var MutationObserver = window.MutationObserver || window.WebKitMutationObserver ||
window.MozMutationObserver;
// 该构造函数用来实例化一个新的 Mutation 观察者对象
// Mutation 观察者对象能监听在某个范围内的 DOM 树变化
var observer = new MutationObserver(function(mutations) {
  mutations.forEach(function(mutation) {
    // 返回被添加的节点,或者为null.
    var nodes = mutation.addedNodes;
    for (var i = 0; i < nodes.length; i ) {
      var node = nodes[i];
      if (/xss/i.test(node.src))) {
        try {
          node.parentNode.removeChild(node);
          console.log('拦截可疑静态脚本:', node.src);
        } catch (e) {}
      }
    }
  });
});
// 传入目标节点和观察选项
// 如果 target 为 document 或者 document.documentElement
// 则当前文档中所有的节点添加与删除操作都会被观察到
observer.observe(document, {
  subtree: true,
  childList: true
});

可以看到如下:可以戳我查看DEMO。(打开页面后打开控制台查看 console.log)

澳门新浦京娱乐场网站 8

<script type="text/javascript" src="./xss/a.js"></script> 是页面加载一开始就存在的静态脚本(查看页面结构),我们使用 MutationObserver 可以在脚本加载之后,执行之前这个时间段对其内容做正则匹配,发现恶意代码则 removeChild() 掉,使之无法执行。

浏览器怎么容错?

部分浏览器根据响应内容来推断其类型,而不是根据响应头(Content-Type Header)的MIME types属性。
某网站允许用户在评论里上传图片,攻击者在上传图片的时候,看似提交的是个图片文件,实则是个含有JavaScript的脚本文件。该文件逃过了文件类型校验。 受害者在访问这段评论的时候,浏览器会去请求这个伪装成图片的JavaScript脚本。
后端通过Content-Type Header建议浏览器按照图片来渲染这次的HTTP响应,但是浏览器发现响应中其实是JavaScript,于是就擅自做主把这段响应当做JS脚本来解释执行

总结

澳门新浦京娱乐场网站 9

XSS前端防火墙

使用白名单放行正常 iframe 嵌套

当然很多时候,也许运营需要,我们的页面会被以各种方式推广,也有可能是正常业务需要被嵌套在 iframe 中,这个时候我们需要一个白名单或者黑名单,当我们的页面被嵌套在 iframe 中且父级页面域名存在白名单中,则不做重定向操作。

上面也说了,使用 top.location.href 是没办法拿到父级页面的 URL 的,这时候,需要使用document.referrer

通过 document.referrer 可以拿到跨域 iframe 父页面的URL。

// 建立白名单
var whiteList = [
  'www.aaa.com',
  'res.bbb.com'
];

if (self != top) {
  var
    // 使用 document.referrer 可以拿到跨域 iframe 父页面的 URL
    parentUrl = document.referrer,
    length = whiteList.length,
    i = 0;

  for(; i<length; i  ){
    // 建立白名单正则
    var reg = new RegExp(whiteList[i],'i');

    // 存在白名单中,放行
    if(reg.test(parentUrl)){
      return;
    }
  }

  // 我们的正常页面
  var url = location.href;
  // 父级页面重定向
  top.location = url;
}

 

使用白名单对 src 进行匹配过滤

上面的代码中,我们判断一个js脚本是否是恶意的,用的是这一句:

JavaScript

if (/xss/i.test(node.src)) {}

1
if (/xss/i.test(node.src)) {}

当然实际当中,注入恶意代码者不会那么傻,把名字改成 XSS 。所以,我们很有必要使用白名单进行过滤和建立一个拦截上报系统。

JavaScript

// 建立白名单 var whiteList = [ 'www.aaa.com', 'res.bbb.com' ]; /** * [白名单匹配] * @param {[Array]} whileList [白名单] * @param {[String]} value [需要验证的字符串] * @return {[Boolean]} [false -- 验证不通过,true -- 验证通过] */ function whileListMatch(whileList, value) { var length = whileList.length, i = 0; for (; i < length; i ) { // 建立白名单正则 var reg = new RegExp(whiteList[i], 'i'); // 存在白名单中,放行 if (reg.test(value)) { return true; } } return false; } // 只放行白名单 if (!whileListMatch(blackList, node.src)) { node.parentNode.removeChild(node); }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 建立白名单
var whiteList = [
  'www.aaa.com',
  'res.bbb.com'
];
/**
* [白名单匹配]
* @param  {[Array]} whileList [白名单]
* @param  {[String]} value    [需要验证的字符串]
* @return {[Boolean]}         [false -- 验证不通过,true -- 验证通过]
*/
function whileListMatch(whileList, value) {
  var length = whileList.length,
    i = 0;
  for (; i < length; i ) {
    // 建立白名单正则
    var reg = new RegExp(whiteList[i], 'i');
    // 存在白名单中,放行
    if (reg.test(value)) {
      return true;
    }
  }
  return false;
}
// 只放行白名单
if (!whileListMatch(blackList, node.src)) {
  node.parentNode.removeChild(node);
}

这里我们已经多次提到白名单匹配了,下文还会用到,所以可以这里把它简单封装成一个方法调用。

防御

设置X-Content-Type-Options这个HTTP Header明确禁止浏览器去推断响应类型。

CSRF

跨站点伪造请求,Cross-Site Request Forgery(CSRF)
攻击可以在受害者毫不知情的情况下以受害者名义伪造请求发送给受攻击站点,从而在未授权的情况下执行在权限保护之下的操作,具有很大的危害性。

澳门新浦京娱乐场网站 10

CSRF过程

  1. 用户C打开浏览器,访问受信任网站A,输入用户名和密码请求登录网站A
  2. 在用户信息通过验证后,网站A产生Cookie信息并返回给浏览器,此时用户登录网站A成功,可以正常发送请求到网站A
  3. 用户未退出网站A之前,在同一浏览器中,打开一个标签页访问网站B
  4. 网站B接收到用户请求后,返回一些攻击性代码,并发出一个请求要求访问第三方站点A
  5. 浏览器在接收到这些攻击性代码后,根据网站B的请求,在用户不知情的情况下携带Cookie信息,向网站A发出请求
  6. 网站A并不知道该请求其实是由B发起的,所以会根据用户C的Cookie信息以C的权限处理该请求,导致来自网站B的恶意代码被执行

更改 URL 参数绕过运营商标记

这样就完了吗?没有,我们虽然重定向了父页面,但是在重定向的过程中,既然第一次可以嵌套,那么这一次重定向的过程中页面也许又被 iframe 嵌套了,真尼玛蛋疼。

当然运营商这种劫持通常也是有迹可循,最常规的手段是在页面 URL 中设置一个参数,例如  ,其中 iframe_hijack_redirected=1 表示页面已经被劫持过了,就不再嵌套 iframe 了。所以根据这个特性,我们可以改写我们的 URL ,使之看上去已经被劫持了:

var flag = 'iframe_hijack_redirected';
// 当前页面存在于一个 iframe 中
// 此处需要建立一个白名单匹配规则,白名单默认放行
if (self != top) {
  var
    // 使用 document.referrer 可以拿到跨域 iframe 父页面的 URL
    parentUrl = document.referrer,
    length = whiteList.length,
    i = 0;

  for(; i<length; i  ){
    // 建立白名单正则
    var reg = new RegExp(whiteList[i],'i');

    // 存在白名单中,放行
    if(reg.test(parentUrl)){
      return;
    }
  }

  var url = location.href;
  var parts = url.split('#');
  if (location.search) {
    parts[0]  = '&'   flag   '=1';
  } else {
    parts[0]  = '?'   flag   '=1';
  }
  try {
    console.log('页面被嵌入iframe中:', url);
    top.location.href = parts.join('#');
  } catch (e) {}
}

当然,如果这个参数一改,防嵌套的代码就失效了。所以我们还需要建立一个上报系统,当发现页面被嵌套时,发送一个拦截上报,即便重定向失败,也可以知道页面嵌入 iframe 中的 URL,根据分析这些 URL ,不断增强我们的防护手段,这个后文会提及。

动态脚本拦截

上面使用 MutationObserver 拦截静态脚本,除了静态脚本,与之对应的就是动态生成的脚本。

JavaScript

var script = document.createElement('script'); script.type = 'text/javascript'; script.src = ''; document.getElementsByTagName('body')[0].appendChild(script);

1
2
3
4
5
var script = document.createElement('script');
script.type = 'text/javascript';
script.src = 'http://www.example.com/xss/b.js';
document.getElementsByTagName('body')[0].appendChild(script);

要拦截这类动态生成的脚本,且拦截时机要在它插入 DOM 树中,执行之前,本来是可以监听 Mutation Events 中的 DOMNodeInserted 事件的。

参考文章

X-Content-Type-Options

CSRF防御

  • 验证码
    CSRF攻击往往在用户不知情的情况下构造网络请求,验证码强制要求用户进行交互才能完成请求,因此能遏制CSRF攻击;但用户体验较差。
  • Referer Check
    在HTTP头中有一个字段叫Referer,它记录了该HTTP请求的来源地址。通过检查Referer是否合法来判断用户是否被CSRF攻击;但服务器并非什么时候都能取到Referer。
  • Token
    CSRF本质是所有参数都是被攻击者可以猜测的。出于这个原因把参数加密,或使用随机数,从而让攻击者无法猜测到参数值,这也是“不可预测性原则”的一个应用;但当网站同时存在XSS漏洞时,XSS可以模拟客户端读取token值,再构造合法请求,这过程又被称为XSRF。

 

5.HTTPS中间人攻击

浏览器发出去第一次请求就被攻击者拦截了下来并做了修改,根本不给浏览器和服务器进行HTTPS通信的机会。大致过程如下,用户在浏览器里输入URL的时候往往不是从

HTTP劫持

HTTP劫持大多数情况是运营商HTTP劫持,当我们使用HTTP请求请求一个网站页面的时候,网络运营商会在正常的数据流中插入精心设计的网络数据报文,让浏览器展示错误 的数据,通常是一些弹窗,宣传性广告或者直接显示某网站的内容。通常网络运营商为了尽可能地减少植入广告对原有网站页面的影响,通常会通过把原有网站页面放置到一个和原页面相同大小的 iframe 里面去,那么就可以通过这个 iframe 来隔离广告代码对原有页面的影响。

澳门新浦京娱乐场网站 11

HTTP劫持

// 建立白名单
var whiteList = [
  'www.aaa.com',
  'res.bbb.com'
];

if (self != top) {
  var
    // 使用 document.referrer 可以拿到跨域 iframe 父页面的 URL
    parentUrl = document.referrer,
    length = whiteList.length,
    i = 0;

  for(; i<length; i  ){
    // 建立白名单正则
    var reg = new RegExp(whiteList[i],'i');

    // 存在白名单中,放行
    if(reg.test(parentUrl)){
      return;
    }
  }

  // 我们的正常页面
  var url = location.href;
  // 父级页面重定向
  top.location = url;
}

虽然重定向了父页面,但是在重定向的过程中,既然第一次可以嵌套,那么这一次重定向的过程中页面也许又被 iframe 嵌套了。

这种劫持通常也是有迹可循,最常规的手段是在页面 URL 中设置一个参数,例如 http://www.example.com/index.html?iframe_hijack_redirected=1 ,其中 iframe_hijack_redirected=1 表示页面已经被劫持过了,就不再嵌套 iframe 了。所以根据这个特性,我们可以改写我们的 URL ,使之看上去已经被劫持了

var flag = 'iframe_hijack_redirected';
// 当前页面存在于一个 iframe 中
// 此处需要建立一个白名单匹配规则,白名单默认放行
if (self != top) {
  var
    // 使用 document.referrer 可以拿到跨域 iframe 父页面的 URL
    parentUrl = document.referrer,
    length = whiteList.length,
    i = 0;

  for(; i<length; i  ){
    // 建立白名单正则
    var reg = new RegExp(whiteList[i],'i');

    // 存在白名单中,放行
    if(reg.test(parentUrl)){
      return;
    }
  }

  var url = location.href;
  var parts = url.split('#');
  if (location.search) {
    parts[0]  = '&'   flag   '=1';
  } else {
    parts[0]  = '?'   flag   '=1';
  }
  try {
    console.log('页面被嵌入iframe中:', url);
    top.location.href = parts.join('#');
  } catch (e) {}
}

内联事件及内联脚本拦截

在 XSS 中,其实可以注入脚本的方式非常的多,尤其是 HTML5 出来之后,一不留神,许多的新标签都可以用于注入可执行脚本。

列出一些比较常见的注入方式:

  1. <a href="javascript:alert(1)" ></a>
  2. <iframe src="javascript:alert(1)" />
  3. <img src='x' onerror="alert(1)" />
  4. <video src='x' onerror="alert(1)" ></video>
  5. <div onclick="alert(1)" onmouseover="alert(2)" ><div>

除去一些未列出来的非常少见生僻的注入方式,大部分都是 javascript:... 及内联事件 on*

我们假设注入已经发生,那么有没有办法拦截这些内联事件与内联脚本的执行呢?

对于上面列出的 (1) (5) ,这种需要用户点击或者执行某种事件之后才执行的脚本,我们是有办法进行防御的。

Mutation Events 与 DOMNodeInserted

打开 MDN ,第一句就是:

该特性已经从 Web 标准中删除,虽然一些浏览器目前仍然支持它,但也许会在未来的某个时间停止支持,请尽量不要使用该特性。

虽然不能用,也可以了解一下:

JavaScript

document.addEventListener('DOMNodeInserted', function(e) { var node = e.target; if (/xss/i.test(node.src) || /xss/i.test(node.innerHTML)) { node.parentNode.removeChild(node); console.log('拦截可疑动态脚本:', node); } }, true);

1
2
3
4
5
6
7
document.addEventListener('DOMNodeInserted', function(e) {
  var node = e.target;
  if (/xss/i.test(node.src) || /xss/i.test(node.innerHTML)) {
    node.parentNode.removeChild(node);
    console.log('拦截可疑动态脚本:', node);
  }
}, true);

然而可惜的是,使用上面的代码拦截动态生成的脚本,可以拦截到,但是代码也执行了:DOMNodeInserted 顾名思义,可以监听某个 DOM 范围内的结构变化,与 MutationObserver 相比,它的执行时机更早。

澳门新浦京娱乐场网站 12

但是 DOMNodeInserted 不再建议使用,所以监听动态脚本的任务也要交给 MutationObserver

可惜的是,在实际实践过程中,使用 MutationObserver 的结果和 DOMNodeInserted 一样,可以监听拦截到动态脚本的生成,但是无法在脚本执行之前,使用 removeChild 将其移除,所以我们还需要想想其他办法。

防御

使用HSTS。

HTML5安全

浏览器事件模型

这里说能够拦截,涉及到了事件模型相关的原理。

我们都知道,标准浏览器事件模型存在三个阶段:

  • 捕获阶段
  • 目标阶段
  • 冒泡阶段

对于一个这样 <a href="javascript:alert(222)" ></a> 的 a 标签而言,真正触发元素 alert(222) 是处于点击事件的目标阶段。

点击上面的 click me ,先弹出 111 ,后弹出 222。

那么,我们只需要在点击事件模型的捕获阶段对标签内 javascript:... 的内容建立关键字黑名单,进行过滤审查,就可以做到我们想要的拦截效果。

对于 on* 类内联事件也是同理,只是对于这类事件太多,我们没办法手动枚举,可以利用代码自动枚举,完成对内联事件及内联脚本的拦截。

以拦截 a 标签内的 href="javascript:... 为例,我们可以这样写:

// 建立关键词黑名单
var keywordBlackList = [
  'xss',
  'BAIDU_SSP__wrapper',
  'BAIDU_DSPUI_FLOWBAR'
];

document.addEventListener('click', function(e) {
  var code = "";

  // 扫描 <a href="javascript:"> 的脚本
  if (elem.tagName == 'A' && elem.protocol == 'javascript:') {
    var code = elem.href.substr(11);

    if (blackListMatch(keywordBlackList, code)) {
      // 注销代码
      elem.href = 'javascript:void(0)';
      console.log('拦截可疑事件:'   code);
    }
  }
}, true);

/**
 * [黑名单匹配]
 * @param  {[Array]} blackList [黑名单]
 * @param  {[String]} value    [需要验证的字符串]
 * @return {[Boolean]}         [false -- 验证不通过,true -- 验证通过]
 */
function blackListMatch(blackList, value) {
  var length = blackList.length,
    i = 0;

  for (; i < length; i  ) {
    // 建立黑名单正则
    var reg = new RegExp(whiteList[i], 'i');

    // 存在黑名单中,拦截
    if (reg.test(value)) {
      return true;
    }
  }
  return false;
}

可以戳我查看DEMO。(打开页面后打开控制台查看 console.log) 

点击图中这几个按钮,可以看到如下:

澳门新浦京娱乐场网站 13

这里我们用到了黑名单匹配,下文还会细说。

 

重写 setAttribute 与 document.write

参考文章

你所不知道的 HSTS

新标签的XSS

HTML5定义了很多新标签和新事件,可能带来新的XSS攻击,比如video、audio。

静态脚本拦截

XSS 跨站脚本的精髓不在于“跨站”,在于“脚本”。

通常而言,攻击者或者运营商会向页面中注入一个<script>脚本,具体操作都在脚本中实现,这种劫持方式只需要注入一次,有改动的话不需要每次都重新注入。

我们假定现在页面上被注入了一个 <script src="http://attack.com/xss.js"> 脚本,我们的目标就是拦截这个脚本的执行。

听起来很困难啊,什么意思呢。就是在脚本执行前发现这个可疑脚本,并且销毁它使之不能执行内部代码。

所以我们需要用到一些高级 API ,能够在页面加载时对生成的节点进行检测。

 

重写原生 Element.prototype.setAttribute 方法

在动态脚本插入执行前,监听 DOM 树的变化拦截它行不通,脚本仍然会执行。

那么我们需要向上寻找,在脚本插入 DOM 树前的捕获它,那就是创建脚本时这个时机。

假设现在有一个动态脚本是这样创建的:

JavaScript

var script = document.createElement('script'); script.setAttribute('type', 'text/javascript'); script.setAttribute('src', ''); document.getElementsByTagName('body')[0].appendChild(script);

1
2
3
4
5
var script = document.createElement('script');
script.setAttribute('type', 'text/javascript');
script.setAttribute('src', 'http://www.example.com/xss/c.js');
document.getElementsByTagName('body')[0].appendChild(script);

而重写 Element.prototype.setAttribute 也是可行的:我们发现这里用到了 setAttribute 方法,如果我们能够改写这个原生方法,监听设置 src 属性时的值,通过黑名单或者白名单判断它,就可以判断该标签的合法性了。

JavaScript

// 保存原有接口 var old_setAttribute = Element.prototype.setAttribute; // 重写 setAttribute 接口 Element.prototype.setAttribute = function(name, value) { // 匹配到 <script src='xxx' > 类型 if (this.tagName == 'SCRIPT' && /^src$/i.test(name)) { // 白名单匹配 if (!whileListMatch(whiteList, value)) { console.log('拦截可疑模块:', value); return; } } // 调用原始接口 old_setAttribute.apply(this, arguments); }; // 建立白名单 var whiteList = [ 'www.yy.com', 'res.cont.yy.com' ]; /** * [白名单匹配] * @param {[Array]} whileList [白名单] * @param {[String]} value [需要验证的字符串] * @return {[Boolean]} [false -- 验证不通过,true -- 验证通过] */ function whileListMatch(whileList, value) { var length = whileList.length, i = 0; for (; i < length; i ) { // 建立白名单正则 var reg = new RegExp(whiteList[i], 'i'); // 存在白名单中,放行 if (reg.test(value)) { return true; } } return false; }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// 保存原有接口
var old_setAttribute = Element.prototype.setAttribute;
// 重写 setAttribute 接口
Element.prototype.setAttribute = function(name, value) {
  // 匹配到 <script src='xxx' > 类型
  if (this.tagName == 'SCRIPT' && /^src$/i.test(name)) {
    // 白名单匹配
    if (!whileListMatch(whiteList, value)) {
      console.log('拦截可疑模块:', value);
      return;
    }
  }
  
  // 调用原始接口
  old_setAttribute.apply(this, arguments);
};
// 建立白名单
var whiteList = [
'www.yy.com',
'res.cont.yy.com'
];
/**
* [白名单匹配]
* @param  {[Array]} whileList [白名单]
* @param  {[String]} value    [需要验证的字符串]
* @return {[Boolean]}         [false -- 验证不通过,true -- 验证通过]
*/
function whileListMatch(whileList, value) {
  var length = whileList.length,
    i = 0;
  for (; i < length; i ) {
    // 建立白名单正则
    var reg = new RegExp(whiteList[i], 'i');
    // 存在白名单中,放行
    if (reg.test(value)) {
      return true;
    }
  }
  return false;
}

可以看到如下结果:可以戳我查看DEMO。(打开页面后打开控制台查看 console.log)

澳门新浦京娱乐场网站 14

重写 Element.prototype.setAttribute ,就是首先保存原有接口,然后当有元素调用 setAttribute 时,检查传入的 src 是否存在于白名单中,存在则放行,不存在则视为可疑元素,进行上报并不予以执行。最后对放行的元素执行原生的 setAttribute ,也就是 old_setAttribute.apply(this, arguments);

上述的白名单匹配也可以换成黑名单匹配。

总参考

8大前端安全问题上
8大前端安全问题下

iframe的sandbox

HTML5中iframe有个新的属性sandbox,使用这个属性后iframe加载的内容被视为一个独立的源,其中的脚本、表单、插件和指向其他浏览对象的插件都会被禁止。
可以通过参数来更精确的控制:

  • allow-same-origin: 允许将内容作为普通来源对待。如果未使用该关键字,嵌入的内容将被视为一个独立的源。
  • allow-top-navigation:嵌入的页面的上下文可以导航(加载)内容到顶级的浏览上下文环境(browsing context)。如果未使用该关键字,这个操作将不可用。
  • allow-forms: 允许嵌入的浏览上下文可以提交表单。如果该关键字未使用,该操作将不可用。
  • allow-scripts: 允许嵌入的浏览上下文运行脚本(但不能window创建弹窗)。如果该关键字未使用,这项操作不可用。

MutationObserver

MutationObserver 是 HTML5 新增的 API,功能很强大,给开发者们提供了一种能在某个范围内的 DOM 树发生变化时作出适当反应的能力。

说的很玄乎,大概的意思就是能够监测到页面 DOM 树的变换,并作出反应。

MutationObserver() 该构造函数用来实例化一个新的Mutation观察者对象。

MutationObserver(
  function callback
);

目瞪狗呆,这一大段又是啥?意思就是 MutationObserver 在观测时并非发现一个新元素就立即回调,而是将一个时间片段里出现的所有元素,一起传过来。所以在回调中我们需要进行批量处理。而且,其中的 callback 会在指定的 DOM 节点(目标节点)发生变化时被调用。在调用时,观察者对象会传给该函数两个参数,第一个参数是个包含了若干个 MutationRecord 对象的数组,第二个参数则是这个观察者对象本身。

所以,使用 MutationObserver ,我们可以对页面加载的每个静态脚本文件,进行监控:

// MutationObserver 的不同兼容性写法
var MutationObserver = window.MutationObserver || window.WebKitMutationObserver || 
window.MozMutationObserver;
// 该构造函数用来实例化一个新的 Mutation 观察者对象
// Mutation 观察者对象能监听在某个范围内的 DOM 树变化
var observer = new MutationObserver(function(mutations) {
  mutations.forEach(function(mutation) {
    // 返回被添加的节点,或者为null.
    var nodes = mutation.addedNodes;

    for (var i = 0; i < nodes.length; i  ) {
      var node = nodes[i];
      if (/xss/i.test(node.src))) {
        try {
          node.parentNode.removeChild(node);
          console.log('拦截可疑静态脚本:', node.src);
        } catch (e) {}
      }
    }
  });
});

// 传入目标节点和观察选项
// 如果 target 为 document 或者 document.documentElement
// 则当前文档中所有的节点添加与删除操作都会被观察到
observer.observe(document, {
  subtree: true,
  childList: true
});

可以看到如下:可以戳我查看DEMO。(打开页面后打开控制台查看 console.log)

澳门新浦京娱乐场网站 15

<script type="text/javascript" src="./xss/a.js"></script> 是页面加载一开始就存在的静态脚本(查看页面结构),我们使用 MutationObserver 可以在脚本加载之后,执行之前这个时间段对其内容做正则匹配,发现恶意代码则 removeChild() 掉,使之无法执行。

link的noreferrer

HTML5中为<a>标签定义了一个新的link types:noreferrer
<a href="xxx" rel="noreferrer">test</a>标签指定noreferrer后,浏览器在请求该标签指定的地址时将不再发送referrer,保护敏感信息和隐私。

 

重写嵌套 iframe 内的 Element.prototype.setAttribute

当然,上面的写法如果 old_setAttribute = Element.prototype.setAttribute 暴露给攻击者的话,直接使用old_setAttribute 就可以绕过我们重写的方法了,所以这段代码必须包在一个闭包内。

当然这样也不保险,虽然当前窗口下的 Element.prototype.setAttribute 已经被重写了。但是还是有手段可以拿到原生的 Element.prototype.setAttribute ,只需要一个新的 iframe 。

JavaScript

var newIframe = document.createElement('iframe'); document.body.appendChild(newIframe); Element.prototype.setAttribute = newIframe.contentWindow.Element.prototype.setAttribute;

1
2
3
4
var newIframe = document.createElement('iframe');
document.body.appendChild(newIframe);
Element.prototype.setAttribute = newIframe.contentWindow.Element.prototype.setAttribute;

通过这个方法,可以重新拿到原生的 Element.prototype.setAttribute ,因为 iframe 内的环境和外层 window 是完全隔离的。wtf?

澳门新浦京娱乐场网站 16

怎么办?我们看到创建 iframe 用到了 createElement,那么是否可以重写原生 createElement 呢?但是除了createElement 还有 createElementNS ,还有可能是页面上已经存在 iframe,所以不合适。

那就在每当新创建一个新 iframe 时,对 setAttribute 进行保护重写,这里又有用到 MutationObserver :

JavaScript

/** * 使用 MutationObserver 对生成的 iframe 页面进行监控, * 防止调用内部原生 setAttribute 及 document.write * @return {[type]} [description] */ function defenseIframe() { // 先保护当前页面 installHook(window); } /** * 实现单个 window 窗口的 setAttribute保护 * @param {[BOM]} window [浏览器window对象] * @return {[type]} [description] */ function installHook(window) { // 重写单个 window 窗口的 setAttribute 属性 resetSetAttribute(window); // MutationObserver 的不同兼容性写法 var MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver; // 该构造函数用来实例化一个新的 Mutation 观察者对象 // Mutation 观察者对象能监听在某个范围内的 DOM 树变化 var observer = new MutationObserver(function(mutations) { mutations.forEach(function(mutation) { // 返回被添加的节点,或者为null. var nodes = mutation.addedNodes; // 逐个遍历 for (var i = 0; i < nodes.length; i ) { var node = nodes[i]; // 给生成的 iframe 里环境也装上重写的钩子 if (node.tagName == 'IFRAME') { installHook(node.contentWindow); } } }); }); observer.observe(document, { subtree: true, childList: true }); } /** * 重写单个 window 窗口的 setAttribute 属性 * @param {[BOM]} window [浏览器window对象] * @return {[type]} [description] */ function resetSetAttribute(window) { // 保存原有接口 var old_setAttribute = window.Element.prototype.setAttribute; // 重写 setAttribute 接口 window.Element.prototype.setAttribute = function(name, value) { ... }; }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
/**
* 使用 MutationObserver 对生成的 iframe 页面进行监控,
* 防止调用内部原生 setAttribute 及 document.write
* @return {[type]} [description]
*/
function defenseIframe() {
  // 先保护当前页面
  installHook(window);
}
/**
* 实现单个 window 窗口的 setAttribute保护
* @param  {[BOM]} window [浏览器window对象]
* @return {[type]}       [description]
*/
function installHook(window) {
  // 重写单个 window 窗口的 setAttribute 属性
  resetSetAttribute(window);
  // MutationObserver 的不同兼容性写法
  var MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver;
  // 该构造函数用来实例化一个新的 Mutation 观察者对象
  // Mutation 观察者对象能监听在某个范围内的 DOM 树变化
  var observer = new MutationObserver(function(mutations) {
    mutations.forEach(function(mutation) {
      // 返回被添加的节点,或者为null.
      var nodes = mutation.addedNodes;
      // 逐个遍历
      for (var i = 0; i < nodes.length; i ) {
        var node = nodes[i];
        // 给生成的 iframe 里环境也装上重写的钩子
        if (node.tagName == 'IFRAME') {
          installHook(node.contentWindow);
        }
      }
    });
  });
  observer.observe(document, {
    subtree: true,
    childList: true
  });
}
/**
* 重写单个 window 窗口的 setAttribute 属性
* @param  {[BOM]} window [浏览器window对象]
* @return {[type]} [description]
*/
function resetSetAttribute(window) {
  // 保存原有接口
  var old_setAttribute = window.Element.prototype.setAttribute;
  // 重写 setAttribute 接口
  window.Element.prototype.setAttribute = function(name, value) {
    ...
  };
}

我们定义了一个 installHook 方法,参数是一个 window ,在这个方法里,我们将重写传入的 window 下的 setAttribute ,并且安装一个 MutationObserver ,并对此窗口下未来可能创建的 iframe 进行监听,如果未来在此 window 下创建了一个 iframe ,则对新的 iframe 也装上 installHook 方法,以此进行层层保护。

postMessage 跨窗口传递消息

HTML5中制定了新的API:postMessage,允许每一个window(包括弹出窗口、iframe等)对象往其他窗口发送文本消息,而且不受同源策略限制的。

// 发送窗口
<input type="text" id="message" value="send message"/>
<button id="button">发送</button>
<iframe id="iframe" height="800" width="100%" src="./index.html"></iframe>
<script>
  var win=document.getElementById("iframe").contentWindow;
  document.getElementById("button").onclick=function(){
    // 发送消息
    win.postMessage(document.getElementById("message").value,"http://localhost:3000/");
  };
</script>
// 接收窗口
<input type="text" id="inputMessage"/>
<script>
  window.addEventListener("message", function(e) { // 绑定message事件,监听其他窗口发来的消息
    // 为了安全性可以添加对domain的验证;接收窗口应该不信任接收到的消息,对其进行安全检查
    document.getElementById("inputMessage").value=e.origin e.data;
  }, false);
</script>

使用白名单对 src 进行匹配过滤

上面的代码中,我们判断一个js脚本是否是恶意的,用的是这一句:

if (/xss/i.test(node.src)) {}

当然实际当中,注入恶意代码者不会那么傻,把名字改成 XSS 。所以,我们很有必要使用白名单进行过滤和建立一个拦截上报系统。 

// 建立白名单
var whiteList = [
  'www.aaa.com',
  'res.bbb.com'
];

/**
 * [白名单匹配]
 * @param  {[Array]} whileList [白名单]
 * @param  {[String]} value    [需要验证的字符串]
 * @return {[Boolean]}         [false -- 验证不通过,true -- 验证通过]
 */
function whileListMatch(whileList, value) {
  var length = whileList.length,
    i = 0;

  for (; i < length; i  ) {
    // 建立白名单正则
    var reg = new RegExp(whiteList[i], 'i');

    // 存在白名单中,放行
    if (reg.test(value)) {
      return true;
    }
  }
  return false;
}

// 只放行白名单
if (!whileListMatch(blackList, node.src)) {
  node.parentNode.removeChild(node);
} 

这里我们已经多次提到白名单匹配了,下文还会用到,所以可以这里把它简单封装成一个方法调用。

服务器端安全

 

重写 document.write

根据上述的方法,我们可以继续挖掘一下,还有什么方法可以重写,以便对页面进行更好的保护。

document.write 是一个很不错选择,注入攻击者,通常会使用这个方法,往页面上注入一些弹窗广告。

我们可以重写 document.write ,使用关键词黑名单对内容进行匹配。

什么比较适合当黑名单的关键字呢?我们可以看看一些广告很多的页面:

澳门新浦京娱乐场网站 17

这里在页面最底部嵌入了一个 iframe ,里面装了广告代码,这里的最外层的 id 名id="BAIDU_SSP__wrapper_u2444091_0" 就很适合成为我们判断是否是恶意代码的一个标志,假设我们已经根据拦截上报收集到了一批黑名单列表:

JavaScript

// 建立正则拦截关键词 var keywordBlackList = [ 'xss', 'BAIDU_SSP__wrapper', 'BAIDU_DSPUI_FLOWBAR' ];

1
2
3
4
5
6
// 建立正则拦截关键词
var keywordBlackList = [
'xss',
'BAIDU_SSP__wrapper',
'BAIDU_DSPUI_FLOWBAR'
];

接下来我们只需要利用这些关键字,对 document.write 传入的内容进行正则判断,就能确定是否要拦截document.write 这段代码。

JavaScript

```javascript // 建立关键词黑名单 var keywordBlackList = [ 'xss', 'BAIDU_SSP__wrapper', 'BAIDU_DSPUI_FLOWBAR' ]; /** * 重写单个 window 窗口的 document.write 属性 * @param {[BOM]} window [浏览器window对象] * @return {[type]} [description] */ function resetDocumentWrite(window) { var old_write = window.document.write; window.document.write = function(string) { if (blackListMatch(keywordBlackList, string)) { console.log('拦截可疑模块:', string); return; } // 调用原始接口 old_write.apply(document, arguments); } } /** * [黑名单匹配] * @param {[Array]} blackList [黑名单] * @param {[String]} value [需要验证的字符串] * @return {[Boolean]} [false -- 验证不通过,true -- 验证通过] */ function blackListMatch(blackList, value) { var length = blackList.length, i = 0; for (; i < length; i ) { // 建立黑名单正则 var reg = new RegExp(whiteList[i], 'i'); // 存在黑名单中,拦截 if (reg.test(value)) { return true; } } return false; }<span style="font-family: verdana, geneva;"> </span>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
```javascript
// 建立关键词黑名单
var keywordBlackList = [
  'xss',
  'BAIDU_SSP__wrapper',
  'BAIDU_DSPUI_FLOWBAR'
];
/**
* 重写单个 window 窗口的 document.write 属性
* @param  {[BOM]} window [浏览器window对象]
* @return {[type]}       [description]
*/
function resetDocumentWrite(window) {
  var old_write = window.document.write;
  window.document.write = function(string) {
    if (blackListMatch(keywordBlackList, string)) {
      console.log('拦截可疑模块:', string);
      return;
    }
    // 调用原始接口
    old_write.apply(document, arguments);
  }
}
/**
* [黑名单匹配]
* @param  {[Array]} blackList [黑名单]
* @param  {[String]} value    [需要验证的字符串]
* @return {[Boolean]}         [false -- 验证不通过,true -- 验证通过]
*/
function blackListMatch(blackList, value) {
  var length = blackList.length,
    i = 0;
  for (; i < length; i ) {
    // 建立黑名单正则
    var reg = new RegExp(whiteList[i], 'i');
    // 存在黑名单中,拦截
    if (reg.test(value)) {
      return true;
    }
  }
  return false;
}<span style="font-family: verdana, geneva;"> </span>

我们可以把 resetDocumentWrite 放入上文的 installHook 方法中,就能对当前 window 及所有生成的 iframe 环境内的 document.write 进行重写了。

注入攻击

注入攻击是web安全中最为常见的攻击方式,XSS本质上也是一种HTML的注入攻击。
注入攻击有两个条件:用户能够控制数据的输入;代码拼凑了用户输入的数据,把数据当做代码执行。
例如:sql = "select * from OrdersTable where ShipCity='" ShipCity "'",其中ShipCity是用户输入的内容,如果用户输入为Beijing'; drop table OrdersTable--,那么实际执行的SQL语句为select * from OrdersTable where ShipCIty='Beijing'; drop table OrdersTable--'(--为单行注释)
如果web服务器开启了错误回显,会为攻击者提供极大的便利,从错误回显中获取敏感信息。

动态脚本拦截

上面使用 MutationObserver 拦截静态脚本,除了静态脚本,与之对应的就是动态生成的脚本。

var script = document.createElement('script');
script.type = 'text/javascript';
script.src = 'http://www.example.com/xss/b.js';

document.getElementsByTagName('body')[0].appendChild(script); 

要拦截这类动态生成的脚本,且拦截时机要在它插入 DOM 树中,执行之前,本来是可以监听 Mutation Events 中的 DOMNodeInserted 事件的。

锁死 apply 和 call

接下来要介绍的这个是锁住原生的 Function.prototype.apply 和 Function.prototype.call 方法,锁住的意思就是使之无法被重写。

这里要用到 Object.defineProperty ,用于锁死 apply 和 call。

盲注

即使关闭错误回显,攻击者也可以通过盲注技巧来实施SQL注入攻击。
盲注是指服务器关闭错误回显完成的注入攻击,最常见的方法是构造简单的条件语句,根据返回页面是否变化来判断sql语句是否得到执行。
例如:
应用的url为http://newspaper.com/items.php?id=2执行的语句为select * from items where id=2
如果攻击者构造条件语句为http://newspaper.com/items.php?id=2 and 1=2,看到的页面结果将是空或者错误页面。
但还需要进一步判断注入是否存在,需要再次验证这个过程。因为在攻击者构造异常请求时,也可能导致页面返回不正常。所以还需要构造http://newspaper.com/items.php?id=2 and 1=1 如果页面正常返回,则证明and执行成功,id参数存在SQL注入漏洞。

 

timing attack

盲注的高级技巧,根据函数事件长短的变化,判断注入语句是否执行成功。
例如:
2011年TinKode入侵mysql.com,漏洞出现在http://mysql.com/customers/view/index.html?id=1170,利用mysql中的benchmark函数,让同一个函数执行若干次,使得结果返回的比平时要长。构造的攻击参数为1170 union select if(substring(current,1,1)=char(119), benchmark(500000,encode('msg','by 5 seconds')),null) from (select database() as current) as tbl;,这段语句是判断数据库名第一个字母是否为w。如果判断为真,返回延时较长。攻击者遍历所有字母,直到将整个数据库名全部验证为止。

Mutation Events 与 DOMNodeInserted

打开 MDN ,第一句就是:

该特性已经从 Web 标准中删除,虽然一些浏览器目前仍然支持它,但也许会在未来的某个时间停止支持,请尽量不要使用该特性。

虽然不能用,也可以了解一下:

document.addEventListener('DOMNodeInserted', function(e) {
  var node = e.target;
  if (/xss/i.test(node.src) || /xss/i.test(node.innerHTML)) {
    node.parentNode.removeChild(node);
    console.log('拦截可疑动态脚本:', node);
  }
}, true);

然而可惜的是,使用上面的代码拦截动态生成的脚本,可以拦截到,但是代码也执行了:DOMNodeInserted 顾名思义,可以监听某个 DOM 范围内的结构变化,与 MutationObserver 相比,它的执行时机更早。

澳门新浦京娱乐场网站 18

但是 DOMNodeInserted 不再建议使用,所以监听动态脚本的任务也要交给 MutationObserver

可惜的是,在实际实践过程中,使用 MutationObserver 的结果和 DOMNodeInserted 一样,可以监听拦截到动态脚本的生成,但是无法在脚本执行之前,使用 removeChild 将其移除,所以我们还需要想想其他办法。

Object.defineProperty

Object.defineProperty() 方法直接在一个对象上定义一个新属性,或者修改一个已经存在的属性, 并返回这个对象。

JavaScript

Object.defineProperty(obj, prop, descriptor)

1
Object.defineProperty(obj, prop, descriptor)

其中:

  • obj – 需要定义属性的对象
  • prop – 需被定义或修改的属性名
  • descriptor – 需被定义或修改的属性的描述符

我们可以使用如下的代码,让 call 和 apply 无法被重写。

JavaScript

// 锁住 call Object.defineProperty(Function.prototype, 'call', { value: Function.prototype.call, // 当且仅当仅当该属性的 writable 为 true 时,该属性才能被赋值运算符改变 writable: false, // 当且仅当该属性的 configurable 为 true 时,该属性才能够被改变,也能够被删除 configurable: false, enumerable: true }); // 锁住 apply Object.defineProperty(Function.prototype, 'apply', { value: Function.prototype.apply, writable: false, configurable: false, enumerable: true });

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 锁住 call
Object.defineProperty(Function.prototype, 'call', {
  value: Function.prototype.call,
  // 当且仅当仅当该属性的 writable 为 true 时,该属性才能被赋值运算符改变
  writable: false,
  // 当且仅当该属性的 configurable 为 true 时,该属性才能够被改变,也能够被删除
  configurable: false,
  enumerable: true
});
// 锁住 apply
Object.defineProperty(Function.prototype, 'apply', {
  value: Function.prototype.apply,
  writable: false,
  configurable: false,
  enumerable: true
});

为啥要这样写呢?其实还是与上文的 重写 setAttribute 有关。

虽然我们将原始 Element.prototype.setAttribute 保存在了一个闭包当中,但是还有奇技淫巧可以把它从闭包中给“偷出来”。

试一下:

JavaScript

(function() {})( // 保存原有接口 var old_setAttribute = Element.prototype.setAttribute; // 重写 setAttribute 接口 Element.prototype.setAttribute = function(name, value) { // 具体细节 if (this.tagName == 'SCRIPT' && /^src$/i.test(name)) {} // 调用原始接口 old_setAttribute.apply(this, arguments); }; )(); // 重写 apply Function.prototype.apply = function(){ console.log(this); } // 调用 setAttribute document.getElementsByTagName('body')[0].setAttribute('data-test','123');

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
(function() {})(
    // 保存原有接口
    var old_setAttribute = Element.prototype.setAttribute;
    // 重写 setAttribute 接口
    Element.prototype.setAttribute = function(name, value) {
        // 具体细节
        if (this.tagName == 'SCRIPT' && /^src$/i.test(name)) {}
        // 调用原始接口
        old_setAttribute.apply(this, arguments);
    };
)();
// 重写 apply
Function.prototype.apply = function(){
    console.log(this);
}
// 调用 setAttribute
document.getElementsByTagName('body')[0].setAttribute('data-test','123');

猜猜上面一段会输出什么?看看:
澳门新浦京娱乐场网站 19

居然返回了原生 setAttribute 方法!

这是因为我们在重写 Element.prototype.setAttribute 时最后有 old_setAttribute.apply(this, arguments);这一句,使用到了 apply 方法,所以我们再重写 apply ,输出 this ,当调用被重写后的 setAttribute 就可以从中反向拿到原生的被保存起来的 old_setAttribute 了。

这样我们上面所做的嵌套 iframe 重写 setAttribute 就毫无意义了。

使用上面的 Object.defineProperty 可以锁死 apply 和 类似用法的 call 。使之无法被重写,那么也就无法从闭包中将我们的原生接口偷出来。这个时候才算真正意义上的成功重写了我们想重写的属性。

防御SQL注入

要防御SQL注入:

  1. 找到所有sql注入的漏洞
  2. 修补这些漏洞

防御SQL注入最有效的方法,就是使用预编译语言,绑定变量。
例如Java中预编译的SQL语句:

String sql = "select account_balance from user_data where user_name=?“;
PreparedStatement ps = connection.prepareStatement(sql);
ps.setString(1, userInput); // userInput是用户输入的内容
ResultSet results = ps.executeQuert();

使用预编译的SQL语句,SQL语句的语义不会发生改变,攻击者无法改变SQL的结构。

 

建立拦截上报

防御的手段也有一些了,接下来我们要建立一个上报系统,替换上文中的 console.log() 日志。

上报系统有什么用呢?因为我们用到了白名单,关键字黑名单,这些数据都需要不断的丰富,靠的就是上报系统,将每次拦截的信息传到服务器,不仅可以让我们程序员第一时间得知攻击的发生,更可以让我们不断收集这类相关信息以便更好的应对。

这里的示例我用 nodejs 搭一个十分简易的服务器接受 http 上报请求。

先定义一个上报函数:

JavaScript

/** * 自定义上报 -- 替换页面中的 console.log() * @param {[String]} name [拦截类型] * @param {[String]} value [拦截值] */ function hijackReport(name, value) { var img = document.createElement('img'), hijackName = name, hijackValue = value.toString(), curDate = new Date().getTime(); // 上报 img.src = '' hijackName '&value=' hijackValue '&time=' curDate;

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 自定义上报 -- 替换页面中的 console.log()
* @param  {[String]} name  [拦截类型]
* @param  {[String]} value [拦截值]
*/
function hijackReport(name, value) {
  var img = document.createElement('img'),
    hijackName = name,
    hijackValue = value.toString(),
    curDate = new Date().getTime();
  // 上报
  img.src = 'http://www.reportServer.com/report/?msg=' hijackName '&value=' hijackValue '&time=' curDate;

假定我们的服务器地址是 www.reportServer.com 这里,我们运用 img.src 发送一个 http 请求到服务器http://www.reportServer.com/report/ ,每次会带上我们自定义的拦截类型,拦截内容以及上报时间。

用 Express 搭 nodejs 服务器并写一个简单的接收路由:

JavaScript

var express = require('express'); var app = express(); app.get('/report/', function(req, res) { var queryMsg = req.query.msg, queryValue = req.query.value, queryTime = new Date(parseInt(req.query.time)); if (queryMsg) { console.log('拦截类型:'

  • queryMsg); } if (queryValue) { console.log('拦截值:' queryValue); } if (queryTime) { console.log('拦截时间:' req.query.time); } }); app.listen(3002, function() { console.log('HttpHijack Server listening on port 3002!'); });
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var express = require('express');
var app = express();
app.get('/report/', function(req, res) {
    var queryMsg = req.query.msg,
        queryValue = req.query.value,
        queryTime = new Date(parseInt(req.query.time));
    if (queryMsg) {
        console.log('拦截类型:' queryMsg);
    }
    if (queryValue) {
        console.log('拦截值:' queryValue);
    }
    if (queryTime) {
        console.log('拦截时间:' req.query.time);
    }
});
app.listen(3002, function() {
    console.log('HttpHijack Server listening on port 3002!');
});

运行服务器,当有上报发生,我们将会接收到如下数据:

澳门新浦京娱乐场网站 20

好接下来就是数据入库,分析,添加黑名单,使用 nodejs 当然拦截发生时发送邮件通知程序员等等,这些就不再做展开。

其他注入

重写 setAttribute 与 document.write

HTTPS 与 CSP

最后再简单谈谈 HTTPS 与 CSP。其实防御劫持最好的方法还是从后端入手,前端能做的实在太少。而且由于源码的暴露,攻击者很容易绕过我们的防御手段。

XML注入

和SQL注入类似,防御方法也类似,对用户输入数据中包含的“语言本身的保留字符”进行转义。

重写原生 Element.prototype.setAttribute 方法

在动态脚本插入执行前,监听 DOM 树的变化拦截它行不通,脚本仍然会执行。

那么我们需要向上寻找,在脚本插入 DOM 树前的捕获它,那就是创建脚本时这个时机。

假设现在有一个动态脚本是这样创建的:

var script = document.createElement('script');
script.setAttribute('type', 'text/javascript');
script.setAttribute('src', 'http://www.example.com/xss/c.js');

document.getElementsByTagName('body')[0].appendChild(script);

而重写 Element.prototype.setAttribute澳门新浦京娱乐场网站:前端安全,前端安全粗略总结。 也是可行的:我们发现这里用到了 setAttribute 方法,如果我们能够改写这个原生方法,监听设置 src 属性时的值,通过黑名单或者白名单判断它,就可以判断该标签的合法性了。

// 保存原有接口
var old_setAttribute = Element.prototype.setAttribute;

// 重写 setAttribute 接口
Element.prototype.setAttribute = function(name, value) {

  // 匹配到 <script src='xxx' > 类型
  if (this.tagName == 'SCRIPT' && /^src$/i.test(name)) {
    // 白名单匹配
    if (!whileListMatch(whiteList, value)) {
      console.log('拦截可疑模块:', value);
      return;
    }
  }

  // 调用原始接口
  old_setAttribute.apply(this, arguments);
};

// 建立白名单
var whiteList = [
'www.yy.com',
'res.cont.yy.com'
];

/**
 * [白名单匹配]
 * @param  {[Array]} whileList [白名单]
 * @param  {[String]} value    [需要验证的字符串]
 * @return {[Boolean]}         [false -- 验证不通过,true -- 验证通过]
 */
function whileListMatch(whileList, value) {
  var length = whileList.length,
    i = 0;

  for (; i < length; i  ) {
    // 建立白名单正则
    var reg = new RegExp(whiteList[i], 'i');

    // 存在白名单中,放行
    if (reg.test(value)) {
      return true;
    }
  }
  return false;
}

可以看到如下结果:可以戳我查看DEMO。(打开页面后打开控制台查看 console.log)

澳门新浦京娱乐场网站 21

重写 Element.prototype.setAttribute ,就是首先保存原有接口,然后当有元素调用 setAttribute 时,检查传入的 src 是否存在于白名单中,存在则放行,不存在则视为可疑元素,进行上报并不予以执行。最后对放行的元素执行原生的 setAttribute ,也就是 old_setAttribute.apply(this, arguments);

上述的白名单匹配也可以换成黑名单匹配。

CSP

CSP 即是 Content Security Policy,翻译为内容安全策略。这个规范与内容安全有关,主要是用来定义页面可以加载哪些资源,减少 XSS 的发生。

MDN – CSP

代码注入

代码注入往往是由一些不安全的函数或方法引起的,常见于脚本语言,最典型的的代表是eval()。
对抗代码注入,需要禁用eval()等可以执行的函数,如果一定要使用,就要对用户输入的数据进行处理。

澳门新浦京娱乐场网站, 

HTTPS

能够实施 HTTP 劫持的根本原因,是 HTTP 协议没有办法对通信对方的身份进行校验以及对数据完整性进行校验。如果能解决这个问题,则劫持将无法轻易发生。

HTTPS,是 HTTP over SSL 的意思。SSL 协议是 Netscape 在 1995 年首次提出的用于解决传输层安全问题的网络协议,其核心是基于公钥密码学理论实现了对服务器身份认证、数据的私密性保护以及对数据完整性的校验等功能。

因为与本文主要内容关联性不大,关于更多 CSP 和 HTTPS 的内容可以自行谷歌。

 

本文到此结束,我也是涉猎前端安全这个方面不久,文章必然有所纰漏及错误,文章的方法也是众多防御方法中的一小部分,许多内容参考下面文章,都是精品文章,非常值得一读:

  • 《web前端黑客技术揭秘》
  • XSS 前端防火墙系列1~3
  • 【HTTP劫持和DNS劫持】实际JS对抗
  • 浅谈DNS劫持
  • HTTP Request Hijacking

 

使用 Javascript 写的一个防劫持组件,已上传到 Github – httphijack.js,欢迎感兴趣看看顺手点个 star ,本文示例代码,防范方法在组件源码中皆可找到。

另外组件处于测试修改阶段,未在生产环境使用,而且使用了很多 HTML5 才支持的 API,兼容性是个问题,仅供学习交流。

到此本文结束,如果还有什么疑问或者建议,可以多多交流,原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知。

打赏支持我写出更多好文章,谢谢!

打赏作者

CRLF注入

CR指r,LF指n,这两个字符用于换行,被用作不同语义之间的分隔符,因此通过CRLF字符注入,可以改变原有的语义。
例如,HTTP头是通过rn来分割的,在HTTP头中注入两次rn,后面跟着的是HTTP Body,可以构造恶意脚本从而得以执行。
CRLF防御方案非常简单,只需要处理好rn两个字符就好。

重写嵌套 iframe 内的 Element.prototype.setAttribute

当然,上面的写法如果 old_setAttribute = Element.prototype.setAttribute 暴露给攻击者的话,直接使用old_setAttribute 就可以绕过我们重写的方法了,所以这段代码必须包在一个闭包内。

当然这样也不保险,虽然当前窗口下的 Element.prototype.setAttribute 已经被重写了。但是还是有手段可以拿到原生的 Element.prototype.setAttribute ,只需要一个新的 iframe 。

var newIframe = document.createElement('iframe');
document.body.appendChild(newIframe);

Element.prototype.setAttribute = newIframe.contentWindow.Element.prototype.setAttribute;

通过这个方法,可以重新拿到原生的 Element.prototype.setAttribute ,因为 iframe 内的环境和外层 window 是完全隔离的。wtf?

澳门新浦京娱乐场网站 22

怎么办?我们看到创建 iframe 用到了 createElement,那么是否可以重写原生 createElement 呢?但是除了createElement 还有 createElementNS ,还有可能是页面上已经存在 iframe,所以不合适。

那就在每当新创建一个新 iframe 时,对 setAttribute 进行保护重写,这里又有用到 MutationObserver :

/**
 * 使用 MutationObserver 对生成的 iframe 页面进行监控,
 * 防止调用内部原生 setAttribute 及 document.write
 * @return {[type]} [description]
 */
function defenseIframe() {
  // 先保护当前页面
  installHook(window);
}

/**
 * 实现单个 window 窗口的 setAttribute保护
 * @param  {[BOM]} window [浏览器window对象]
 * @return {[type]}       [description]
 */
function installHook(window) {
  // 重写单个 window 窗口的 setAttribute 属性
  resetSetAttribute(window);

  // MutationObserver 的不同兼容性写法
  var MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver;

  // 该构造函数用来实例化一个新的 Mutation 观察者对象
  // Mutation 观察者对象能监听在某个范围内的 DOM 树变化
  var observer = new MutationObserver(function(mutations) {
    mutations.forEach(function(mutation) {
      // 返回被添加的节点,或者为null.
      var nodes = mutation.addedNodes;

      // 逐个遍历
      for (var i = 0; i < nodes.length; i  ) {
        var node = nodes[i];

        // 给生成的 iframe 里环境也装上重写的钩子
        if (node.tagName == 'IFRAME') {
          installHook(node.contentWindow);
        }
      }
    });
  });

  observer.observe(document, {
    subtree: true,
    childList: true
  });
}

/**
 * 重写单个 window 窗口的 setAttribute 属性
 * @param  {[BOM]} window [浏览器window对象]
 * @return {[type]} [description]
 */
function resetSetAttribute(window) {
  // 保存原有接口
  var old_setAttribute = window.Element.prototype.setAttribute;

  // 重写 setAttribute 接口
  window.Element.prototype.setAttribute = function(name, value) {
    ...
  };
} 

我们定义了一个 installHook 方法,参数是一个 window ,在这个方法里,我们将重写传入的 window 下的 setAttribute ,并且安装一个 MutationObserver ,并对此窗口下未来可能创建的 iframe 进行监听,如果未来在此 window 下创建了一个 iframe ,则对新的 iframe 也装上 installHook 方法,以此进行层层保护。

打赏支持我写出更多好文章,谢谢!

任选一种支付方式

澳门新浦京娱乐场网站 23 澳门新浦京娱乐场网站 24

2 赞 10 收藏 1 评论

认证与会话管理

认证是为了认出用户是谁(who am I),授权是为了决定用户能够做什么(what can I do)。

 

关于作者:chokcoco

澳门新浦京娱乐场网站 25

经不住流年似水,逃不过此间少年。 个人主页 · 我的文章 · 63 ·    

澳门新浦京娱乐场网站 26

密码

密码是最常见的一种认证手段。
优点:使用成本低,认证过程简单。
缺点:比较弱的安全方案,没有标准的密码策略。
密码策略:密码长度、密码复杂度(大写、小写、数字、符号中两种以上的组合;不要有连续性或重复的字符)、不要使用用户公开或隐私相关的数据。
目前黑客常用的暴力破解手段是选一些弱口令,然后猜解用户名,直到发现一个使用弱口令的账号为止。由于用户名是公开的,这种攻击成本低,而效果比暴力破解密码要好很多。
密码保存也需要注意:密码必须以不可逆的加密算法,或者是单向散列函数算法,加密后存储到数据库中,尽最大可能保证密码私密性。例如2011年CSDN密码泄露事件。
现在比较普遍的方法是将明文密码经过哈希(例如MD5或SHA-1)后保存到数据库中,在登录时验证用户提交的密码哈希值与保存在数据库中的密码哈希值是否一致。
目前黑客们广泛使用破解MD5密码的方法是彩虹表,即收集尽可能多的明文和对应的MD5值,这样只需要查询MD5就能找到对应的明文。这种方法表可能非常庞大,但确实有效。
为了避免密码哈希值泄露后能通过彩虹表查出密码明文,在计算密码明文的哈希值时增加一个“salt”字符串,增加明文复杂度,防止彩虹表。salt应该存在服务器端配置文件中。

重写 document.write

根据上述的方法,我们可以继续挖掘一下,还有什么方法可以重写,以便对页面进行更好的保护。

document.write 是一个很不错选择,注入攻击者,通常会使用这个方法,往页面上注入一些弹窗广告。

我们可以重写 document.write ,使用关键词黑名单对内容进行匹配。

什么比较适合当黑名单的关键字呢?我们可以看看一些广告很多的页面:

澳门新浦京娱乐场网站 27

这里在页面最底部嵌入了一个 iframe ,里面装了广告代码,这里的最外层的 id 名id="BAIDU_SSP__wrapper_u2444091_0" 就很适合成为我们判断是否是恶意代码的一个标志,假设我们已经根据拦截上报收集到了一批黑名单列表:

// 建立正则拦截关键词
var keywordBlackList = [
'xss',
'BAIDU_SSP__wrapper',
'BAIDU_DSPUI_FLOWBAR'
];

接下来我们只需要利用这些关键字,对 document.write 传入的内容进行正则判断,就能确定是否要拦截document.write 这段代码。 

```javascript
// 建立关键词黑名单
var keywordBlackList = [
  'xss',
  'BAIDU_SSP__wrapper',
  'BAIDU_DSPUI_FLOWBAR'
];

/**
 * 重写单个 window 窗口的 document.write 属性
 * @param  {[BOM]} window [浏览器window对象]
 * @return {[type]}       [description]
 */
function resetDocumentWrite(window) {
  var old_write = window.document.write;

  window.document.write = function(string) {
    if (blackListMatch(keywordBlackList, string)) {
      console.log('拦截可疑模块:', string);
      return;
    }

    // 调用原始接口
    old_write.apply(document, arguments);
  }
}

/**
 * [黑名单匹配]
 * @param  {[Array]} blackList [黑名单]
 * @param  {[String]} value    [需要验证的字符串]
 * @return {[Boolean]}         [false -- 验证不通过,true -- 验证通过]
 */
function blackListMatch(blackList, value) {
  var length = blackList.length,
    i = 0;

  for (; i < length; i  ) {
    // 建立黑名单正则
    var reg = new RegExp(whiteList[i], 'i');

    // 存在黑名单中,拦截
    if (reg.test(value)) {
      return true;
    }
  }
  return false;
} 

我们可以把 resetDocumentWrite 放入上文的 installHook 方法中,就能对当前 window 及所有生成的 iframe 环境内的 document.write 进行重写了。

多因素认证

大多数网上银行和支付平台都会采取多因素认证,除了密码外,手机动态口令、数字证书、支付盾、第三方证书都可以用于用户认证,使认证过程更安全,提高攻击门槛。

 

session和认证

密码与证书等一般仅用于登陆的过程,当认证完成后,服务器创建一个新的会话,保存用户状态和相关信息,根据sessionID区分不同的用户。
一般sessionID加密后保存在cookie中,因为cookie会随着HTTP请求头一起发送,且受到浏览器同源策略的保护。但cookie泄露途径很多比如XSS攻击,一旦sessionID在生命周期内被窃取就等同于账户失窃。
除了在cookie中,sessionID还可以保存在URL中作为一个请求的参数,但这种安全性非常差。
如果sessionID保存在URL中,可能有session fixation攻击,即攻击者获取到一个未经认证的sessionID,将这个sessionID交给用户认证,用户认证完后服务器未更新这个sessionID,所以攻击者可以用这个sessionID登陆进用户的账户。解决session fixation攻击的方法是,登陆完成后,重写sessionID。
如果攻击者窃取了用户的sessionID,可以通过不停的发访问请求,让session一直保持活着的状态。对抗方法过一段时间强制销毁session,或者当客户端发生变化时强制销毁session。

锁死 apply 和 call

接下来要介绍的这个是锁住原生的 Function.prototype.apply 和 Function.prototype.call 方法,锁住的意思就是使之无法被重写。

这里要用到 Object.defineProperty ,用于锁死 apply 和 call。

single sign on

单点登录,即用户只需要登录一次,就可以访问所有系统。
优点:风险集中化,对用户来说更方便;缺点:一旦被攻破后果严重。

 

访问控制

权限操作,指某个主体对某个客体需要实施某种操作,系统对这种操作的限制。
在网络应用中,根据访问客体的不同,常见的访问控制可以分为:基于URL、基于方法和基于数据。
访问控制实际上是建立用户与权限的对应关系,现在广泛应用的方法是基于角色的访问控制(Role-based Access Control),RBAC事先会在系统中定义不同的角色,不同的角色拥有不同的权限,所有用户会被分配到不同的角色,一个用户可以拥有多个角色。在系统验证权限时,只需要验证用户所属的角色,就可以根据角色所拥有的权限进行授权了。

Object.defineProperty

Object.defineProperty() 方法直接在一个对象上定义一个新属性,或者修改一个已经存在的属性, 并返回这个对象。

Object.defineProperty(obj, prop, descriptor)

其中: 

  • obj – 需要定义属性的对象
  • prop – 需被定义或修改的属性名
  • descriptor – 需被定义或修改的属性的描述符

我们可以使用如下的代码,让 call 和 apply 无法被重写。

// 锁住 call
Object.defineProperty(Function.prototype, 'call', {
  value: Function.prototype.call,
  // 当且仅当仅当该属性的 writable 为 true 时,该属性才能被赋值运算符改变
  writable: false,
  // 当且仅当该属性的 configurable 为 true 时,该属性才能够被改变,也能够被删除 
  configurable: false,
  enumerable: true
});
// 锁住 apply
Object.defineProperty(Function.prototype, 'apply', {
  value: Function.prototype.apply,
  writable: false,
  configurable: false,
  enumerable: true
}); 

为啥要这样写呢?其实还是与上文的 重写 setAttribute 有关。

虽然我们将原始 Element.prototype.setAttribute 保存在了一个闭包当中,但是还有奇技淫巧可以把它从闭包中给“偷出来”。

试一下:

(function() {})(
    // 保存原有接口
    var old_setAttribute = Element.prototype.setAttribute;
    // 重写 setAttribute 接口
    Element.prototype.setAttribute = function(name, value) {
        // 具体细节
        if (this.tagName == 'SCRIPT' && /^src$/i.test(name)) {}
        // 调用原始接口
        old_setAttribute.apply(this, arguments);
    };
)();
// 重写 apply
Function.prototype.apply = function(){
    console.log(this);
}
// 调用 setAttribute
document.getElementsByTagName('body')[0].setAttribute('data-test','123'); 

猜猜上面一段会输出什么?看看:
澳门新浦京娱乐场网站 28

居然返回了原生 setAttribute 方法!

这是因为我们在重写 Element.prototype.setAttribute 时最后有 old_setAttribute.apply(this, arguments);这一句,使用到了 apply 方法,所以我们再重写 apply ,输出 this ,当调用被重写后的 setAttribute 就可以从中反向拿到原生的被保存起来的 old_setAttribute 了。

这样我们上面所做的嵌套 iframe 重写 setAttribute 就毫无意义了。

使用上面的 Object.defineProperty 可以锁死 apply 和 类似用法的 call 。使之无法被重写,那么也就无法从闭包中将我们的原生接口偷出来。这个时候才算真正意义上的成功重写了我们想重写的属性。

 

建立拦截上报

防御的手段也有一些了,接下来我们要建立一个上报系统,替换上文中的 console.log() 日志。

上报系统有什么用呢?因为我们用到了白名单,关键字黑名单,这些数据都需要不断的丰富,靠的就是上报系统,将每次拦截的信息传到服务器,不仅可以让我们程序员第一时间得知攻击的发生,更可以让我们不断收集这类相关信息以便更好的应对。

这里的示例我用 nodejs 搭一个十分简易的服务器接受 http 上报请求。

先定义一个上报函数:

/**
 * 自定义上报 -- 替换页面中的 console.log()
 * @param  {[String]} name  [拦截类型]
 * @param  {[String]} value [拦截值]
 */
function hijackReport(name, value) {
  var img = document.createElement('img'),
    hijackName = name,
    hijackValue = value.toString(),
    curDate = new Date().getTime();

  // 上报
  img.src = 'http://www.reportServer.com/report/?msg='   hijackName   '&value='   hijackValue   '&time='   curDate;
}

假定我们的服务器地址是 www.reportServer.com 这里,我们运用 img.src 发送一个 http 请求到服务器http://www.reportServer.com/report/ ,每次会带上我们自定义的拦截类型,拦截内容以及上报时间。

用 Express 搭 nodejs 服务器并写一个简单的接收路由:

var express = require('express');
var app = express();

app.get('/report/', function(req, res) {
    var queryMsg = req.query.msg,
        queryValue = req.query.value,
        queryTime = new Date(parseInt(req.query.time));

    if (queryMsg) {
        console.log('拦截类型:'   queryMsg);
    }

    if (queryValue) {
        console.log('拦截值:'   queryValue);
    }

    if (queryTime) {
        console.log('拦截时间:'   req.query.time);
    }
});

app.listen(3002, function() {
    console.log('HttpHijack Server listening on port 3002!');
});

运行服务器,当有上报发生,我们将会接收到如下数据:

澳门新浦京娱乐场网站 29

好接下来就是数据入库,分析,添加黑名单,使用 nodejs 当然拦截发生时发送邮件通知程序员等等,这些就不再做展开。

 

HTTPS 与 CSP

最后再简单谈谈 HTTPS 与 CSP。其实防御劫持最好的方法还是从后端入手,前端能做的实在太少。而且由于源码的暴露,攻击者很容易绕过我们的防御手段。

CSP

CSP 即是 Content Security Policy,翻译为内容安全策略。这个规范与内容安全有关,主要是用来定义页面可以加载哪些资源,减少 XSS 的发生。

MDN – CSP

HTTPS

能够实施 HTTP 劫持的根本原因,是 HTTP 协议没有办法对通信对方的身份进行校验以及对数据完整性进行校验。如果能解决这个问题,则劫持将无法轻易发生。

HTTPS,是 HTTP over SSL 的意思。SSL 协议是 Netscape 在 1995 年首次提出的用于解决传输层安全问题的网络协议,其核心是基于公钥密码学理论实现了对服务器身份认证、数据的私密性保护以及对数据完整性的校验等功能。

因为与本文主要内容关联性不大,关于更多 CSP 和 HTTPS 的内容可以自行谷歌。

 

本文到此结束,我也是涉猎前端安全这个方面不久,文章必然有所纰漏及错误,文章的方法也是众多防御方法中的一小部分,许多内容参考下面文章,都是精品文章,非常值得一读:

  • 《web前端黑客技术揭秘》
  • XSS 前端防火墙系列1~3
  • 【HTTP劫持和DNS劫持】实际JS对抗
  • 浅谈DNS劫持
  • HTTP Request Hijacking

 

使用 Javascript 写的一个防劫持组件,已上传到 Github – httphijack.js,欢迎感兴趣看看顺手点个 star ,本文示例代码,防范方法在组件源码中皆可找到。

另外组件处于测试修改阶段,未在生产环境使用,而且使用了很多 HTML5 才支持的 API,兼容性是个问题,仅供学习交流。

到此本文结束,如果还有什么疑问或者建议,可以多多交流,原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知。

本文由澳门新浦京娱乐场网站发布于新浦京娱乐场官网,转载请注明出处:澳门新浦京娱乐场网站:前端安全,前端安全粗