详解前端安全之JavaScript防http劫持与XSS

HTTP劫持、DNS劫持与XSS

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

HTTP劫持

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

DNS劫持

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

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

XSS跨站脚本

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

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

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

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

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

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

window.self

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

window.top

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

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

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

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

使用白名单放行正常 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;
}

更改 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) {}
}

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

内联事件及内联脚本拦截

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

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

1.<a href="javascript:alert(1)" rel="external nofollow" _fcksavedurl="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 onmouseover="alert(2)" ><div>

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

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

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

浏览器事件模型

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

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

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

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

<a href="javascript:alert(222)" rel="external nofollow"  rel="external nofollow" >Click Me</a>

<script>
  document.addEventListener('click', function(e) {
    alert(111);
  }, true);
</script>

点击上面的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:" rel="external nofollow" > 的脚本
  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;
}

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

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

静态脚本拦截

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

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

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

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

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

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
});

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

使用白名单对 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);
} 

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

动态脚本拦截

上面使用 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事件的。

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相比,它的执行时机更早。

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

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

重写 setAttribute 与 document.write

重写原生 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;
}

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

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

重写嵌套 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?

怎么办?我们看到创建 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方法,以此进行层层保护。

重写 document.write

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

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

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

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

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

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

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

// 建立关键词黑名单
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进行重写了。

锁死 apply 和 call

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

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

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'); 

猜猜上面一段会输出什么?看看:

居然返回了原生 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!');
});

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

好接下来就是数据入库,分析,添加黑名单,使用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 的内容可以自行谷歌。

以上就是详解前端安全之JavaScript防http劫持与XSS的详细内容,更多关于前端安全之JavaScript防http劫持与XSS的资料请关注我们其它相关文章!

(0)

相关推荐

  • JSP Struts过滤xss攻击的解决办法

    JSP Struts过滤xss攻击的解决办法 本方案采用struts2的拦截器过滤,将提交上来的参数转码来解决. 配置struts.xml <package name="default" namespace="/" extends="struts-default, json-default"> <!-- 配置拦截器 --> <interceptors> <!-- 定义xss拦截器 --> <i

  • JSP使用过滤器防止Xss漏洞

    在用java进行web业务开发的时候,对于页面上接收到的参数,除了极少数是步可预知的内容外,大量的参数名和参数值都是不会出现触发Xss漏洞的字符.而通常为了避免Xss漏洞,都是开发人员各自在页面输出和数据入库等地方加上各种各样的encode方法来避免Xss问题.而由于开发人员的水平不一,加上在编写代码的过程中安全意识的差异,可能会粗心漏掉对用户输入内容进行encode处理.针对这种大量参数是不可能出现引起Xss和SQL注入漏洞的业务场景下,因此可以使用一个适用大多数业务场景的通用处理方法,牺牲少

  • 用js屏蔽被http劫持的浮动广告实现方法

    最近发现网站经常在右下角弹出一个浮动广告,开始的时候以为只是浏览器的广告. 后来越来越多同事反映在家里不同浏览器也会出现广告.然后深入检查了下,发现网站竟然被劫持了. 然后百度了一大堆资料,什么http劫持.dns劫持.运营商劫持之类的,确定真的是中招了.看图: 真是偷梁换柱啊,被插入广告代码了.真是无良奸商,什么都做得出. 然并卵,最重要的解决办法是啥?然后把问题扔给了运维的同事. 最终结果是解决不了.没错,就是这么的坑爹.除非采用https.网上那些什么打电话.发信投诉之类的貌似没啥用.可能

  • 防止xss和sql注入:JS特殊字符过滤正则

    复制代码 代码如下: function stripscript(s) { var pattern = new RegExp("[%--`~!@#$^&*()=|{}':;',\\[\\].<>/?~!@#¥--&*()--|{}[]'::""'.,.?]")        //格式 RegExp("[在中间定义特殊过滤字符]")var rs = ""; for (var i = 0; i <

  • JS写XSS cookie stealer来窃取密码的步骤详解

    JavaScript是web中最常用的脚本开发语言,js可以自动执行站点组件,管理站点内容,在web业内实现其他有用的函数.JS可以有很多的函数可以用做恶意用途,包括窃取含有密码等内容的用户cookie. Cookie是站点请求和保持特定访问页面的信息.Cookie含有访问的方式.时间.用户名密码等认证信息等.当用户访问给定站点时,必须使用cookie:如果攻击者可以拦截cookie,就可以利用cookie窃取用户的一些信息.对某个特定的域名,使用JS可以保存或修改用户的cookie.也就是说,

  • JSP安全开发之XSS漏洞详解

    前言 大家好,好男人就是我,我就是好男人,我就是-0nise.在各大漏洞举报平台,我们时常会看到XSS漏洞.那么问题来了,为何会出现这种漏洞?出现这种漏洞应该怎么修复? 正文 1.XSS?XSS?XSS是什么鬼? XSS又叫跨站脚本攻击(Cross Site Scripting),我不会告诉他原本是叫CSS的,但是为了不和我们所用的层叠样式表(Cascading Style Sheets)CSS搞混.CSS(跨站脚本攻击),CSS(层叠样式表)傻傻分不清.所以就叫XSS咯. 2.XSS的危害是什

  • JSP过滤器防止Xss漏洞的实现方法(分享)

    在用java进行web业务开发的时候,对于页面上接收到的参数,除了极少数是步可预知的内容外,大量的参数名和参数值都是不会出现触发Xss漏洞的字符.而通常为了避免Xss漏洞,都是开发人员各自在页面输出和数据入库等地方加上各种各样的encode方法来避免Xss问题.而由于开发人员的水平不一,加上在编写代码的过程中安全意识的差异,可能会粗心漏掉对用户输入内容进行encode处理.针对这种大量参数是不可能出现引起Xss和SQL注入漏洞的业务场景下,因此可以使用一个适用大多数业务场景的通用处理方法,牺牲少

  • 详解前端安全之JavaScript防http劫持与XSS

    HTTP劫持.DNS劫持与XSS 先简单讲讲什么是 HTTP 劫持与 DNS 劫持. HTTP劫持 什么是HTTP劫持呢,大多数情况是运营商HTTP劫持,当我们使用HTTP请求请求一个网站页面的时候,网络运营商会在正常的数据流中插入精心设计的网络数据报文,让客户端(通常是浏览器)展示"错误"的数据,通常是一些弹窗,宣传性广告或者直接显示某网站的内容,大家应该都有遇到过. DNS劫持 DNS 劫持就是通过劫持了 DNS 服务器,通过某些手段取得某域名的解析记录控制权,进而修改此域名的解析

  • 一文快速详解前端框架 Vue 最强大的功能

    组件是 vue.js最强大的功能之一,而组件实例的作用域是相互独立的,这就意味着不同组件之间的数据无法相互引用.一般来说,组件可以有以下几种关系: 如上图所示,A 和 B.B 和 C.B 和 D 都是父子关系,C 和 D 是兄弟关系,A 和 C 是隔代关系(可能隔多代). 针对不同的使用场景,如何选择行之有效的通信方式?这是我们所要探讨的主题.本文总结了vue组件间通信的几种方式,如props. $emit/ $on.vuex. $parent / $children. $attrs/ $lis

  • 详解前端任务构建利器Gulp.js使用指南

    概述 在软件开发中,任务运行器的好处是不言而喻的.它们可以帮助自动运行常见的冗长的任务,让你可以专注于更重要的事情中,比如敲出很棒的代码.说的严肃点,自动运行一些比如图片压缩.代码压缩.单元测试以及更多的任务的技能,简直就是节省时间的利器. 对于很多前端开发者而言,时下使用最多的任务管理器就是Grunt了,一个可以让你在Gruntfile.js文件中使用JavaScript定义各种运行任务的工具.基本上,只要你了解JavaScript,创建一个Grunt任务是非常简单直接的事情.丰富的第三方插件

  • 详解前端自动化工具gulp自动添加版本号

    之前,我介绍了学习安装并配置前端自动化工具Gulp,觉得gulp确实比grunt的配置简单很多,于是我决定再深入学习一下gulp,就去网上查了资料,发现gulp还可以自动添加版本号,这个功能就为我平时在更新css或js时老是在客户端存在缓存导致更新后的效果无法实时展现的苦恼.所以就赶紧去试了一下,果真可以,很高兴啊,真是为项目开发,为效果的快速展现提供了很多的便利. 实现原理: 1.修改js和css文件: 2.通过对js,css文件内容进行hash运算,生成一个文件的唯一hash字符串(如果文件

  • 详解前端路由实现与react-router使用姿势

    路由 对于有过SPA开发经验的人来说,路由这个名词并不陌生,前端的路由和后端的路由在实现技术上不一样,但是原理都是一样的.在 HTML5 的 history API 出现之前,前端的路由都是通过 hash 来实现的,hash 能兼容低版本的浏览器.它的 URI 规则中需要带上 #.Web 服务并不会解析 hash,也就是说 # 后的内容 Web 服务都会自动忽略,但是 JavaScript 是可以通过 window.location.hash 读取到的,读取到路径加以解析之后就可以响应不同路径的

  • 详解前端到底可以用nginx做什么

    目录 前言 location的匹配规则 history模式.跨域.缓存.反向代理 以目录去区分多个history单文件 负载均衡 灰度部署 优雅降级 webp根据浏览器自动降级为png 为什么要用nginx,nginx有什么特点? 总结 前言 这篇文章是收集我在工作中经常会用到的nginx相关知识点,本文并不是基础知识的讲解更多的是一些方案中的简单实现. location的匹配规则 = 表示精确匹配.只有请求的url路径与后面的字符串完全相等时,才会命中. ^~ 表示如果该符号后面的字符是最佳匹

  • 详解前端构建工具gulpjs的使用介绍及技巧

    gulpjs是一个前端构建工具,与gruntjs相比,gulpjs无需写一大堆繁杂的配置参数,API也非常简单,学习起来很容易,而且gulpjs使用的是nodejs中stream来读取和操作数据,其速度更快.如果你还没有使用过前端构建工具,或者觉得gruntjs太难用的话,那就尝试一下gulp吧. 1.gulp的安装 首先确保你已经正确安装了nodejs环境.然后以全局方式安装gulp: npm install -g gulp 全局安装gulp后,还需要在每个要使用gulp的项目中都单独安装一次

  • javascript中json基础知识详解

    大致介绍 JSON(JavaScript Object Notation  JavaScript对象表示法),JSON是一种数据格式,不是一种编程语言.虽然它的名字中有JavaScript但是它却不属于JavaScript,就像Java和JavaScript的关系一样.而且,并不是只有JavaScript才使用它,毕竟 JSON 只是一种数据格式.很多编程语言都有针对 JSON 的解析器和序列化器. JSON是由Douglas Crockford在2001年提出,为了取代XML 语法 JSON的

  • 微信小程序中使用Promise进行异步流程处理的实例详解

    微信小程序中使用Promise进行异步流程处理的实例详解 我们知道,JavaScript是单进程执行的,同步操作会对程序的执行进行阻塞处理.比如在浏览器页面程序中,如果一段同步的代码需要执行很长时间(比如一个很大的循环操作),则页面会产生卡死的现象. 所以,在JavaScript中,提供了一些异步特性,为程序提供了性能和体验上的益处,比如可以将代码放到setTimeout()中执行:或者在网页中,我们使用Ajax的方式向服务器端做异步数据请求.这些异步的代码不会阻塞当前的界面主进程,界面还是可以

  • 微信小程序 支付功能实现PHP实例详解

    微信小程序 支付功能实现PHP实例详解 前端代码: wx.request({ url: 'https://www.yourhost.com/weixin/WeiActivity/payJoinfee',//改成你自己的链接 header: { 'Content-Type': 'application/x-www-form-urlencoded' }, method:'POST', success: function(res) { console.log(res.data); console.lo

随机推荐