关于javascript:你以为你请求的就是你想请求的吗

3次阅读

共计 5910 个字符,预计需要花费 15 分钟才能阅读完成。

在当今 SPA 利用风行的状况下,页面上的所有货色都是通过 javascript 进行加载,本文将带你一步一步截获用户申请,并批改申请地址。

咱们次要应用的办法为 Hook 原生接口进行接口调用拦挡;在拦挡前,先定义一个 URL 批改的函数,对立将 URL 申请中的 before 批改为 after,你在你的理论解决中可能会更加简单。

function srcHook(url) {let nUrl = url.replace("hook-before", "hook-after");
    return nUrl;
}

Ajax 申请

在前端中,个别是通过 Ajax 向后盾申请数据,所以首要须要拦挡的就是 Ajax 的申请。

先来看一下如何收回一个 Ajax 申请:

var xhr = new XMLHttpRequest();
xhr.timeout = 3000;
xhr.ontimeout = function (event) {alert("申请超时!");
}
xhr.open('GET', '/data/hook-before.txt');
xhr.send();
xhr.onreadystatechange = function () {if (xhr.readyState == 4 && xhr.status == 200) {resolve(xhr.responseText);
         WriteLogs("==== 响应" + xhr.responseText);
    }
}

能够看到,传入 URL 参数的办法是 xhr.open,所以咱们重写XMLHttpRequestopen办法进行拦挡。重写前,须要先保留一下原生办法。

好了,当初开始正式 Hook 了:

var $open = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function () {if (srcHook) {var src = srcHook(arguments[1]);
    if (src === false) return;
    if (src) {arguments[1] = src;
    }
  }
  return $open.apply(this, arguments);
}

是的,就是这么简略,在重写的 open 办法中,对 URL 参数进行批改,而后调用原生办法。

通过咱们的日志信息,能够看到,拜访批改曾经胜利:

没问题,Hook 胜利。然而,你看上面,还有一个 fetch 类型的申请没有 Hook 到,别着急,马上解决它。

依然首先来看一下 fetch 的调用办法:

fetch("/data/hook-before.txt")
.then(function(response){return response.text();
}).then(function(text){alert(text);
})

fetch是一个全局函数,第一个参数为须要申请的网址。咱们只须要重写 window 对象上的 fetch 函数即可。

var $fetch = window.fetch;
window.fetch = function () {if (srcHook) {var src = srcHook(arguments[0]);
    if (src === false) return;
    if (src) {arguments[0] = src;
    }
  }
  return $fetch.apply(window, arguments);
}

这下没问题了,两种申请形式都拦挡了。

DOM 申请

对于失常的 Ajax 申请,咱们曾经进行了解决,但在有些状况下,会在页面中应用 JSONP 来进行跨域申请。

咱们依然是先来看一下 JSONP 的实现:

var url="/data/hook-before.js";
var script = document.createElement('script');
script.setAttribute('src', url);
document.getElementsByTagName('head')[0].appendChild(script);

能够看出,JSONP 的实质是向 DOM 中插入一个 SCRIPT 的 Element。从代码中,我轻松的找到的 Hook 点,Element 实例的 setAttribute 办法。

var $setAttribute = Element.prototype.setAttribute;
Element.prototype.setAttribute = function () {if (this.tagName=="SCRIPT"&&arguments[0]=="src"&&srcHook) {var src = srcHook(arguments[1]);
    if (src === false) return;
    if (src) {arguments[1] = src;
    }
  }
  return $setAttribute.apply(this, arguments);
}

和 xhr 的 hook 齐全一样。

通过同样的办法,也能够把 imglinkiframea 给 hook 掉。

然而,下面的 hook 如同也差了点啥。请看上面的代码:

var url="/data/hook-before.js";
var script = document.createElement('script');
script.src=url;
document.getElementsByTagName('head')[0].appendChild(script);

没错,不调用 setAttribute 办法一样能够设置src

先看一看 src 在原型链上的定义:

{get: ƒ, set: ƒ, enumerable: true, configurable: true}

通过定义能够晓得 src 的属性描述符 (property descriptor) 就能够重写的,这下好办了,咱们重写一下 srcsetter

var descriptor=Object.getOwnPropertyDescriptor(HTMLScriptElement.prototype, "src");
var setter=descriptor["set"];
descriptor["set"]=function(value){if (srcHook) {var src = srcHook(arguments[0]);
    if (src === false) return;
    if (src) {arguments[0] = src;
    }
  }
  return setter.apply(this, arguments);
}
descriptor["configurable"]=false;
// 因为 src 的 set 有可能会被其它脚本批改回去,此处通过设置 configurable=false 来强行禁止批改
Object.defineProperty(HTMLScriptElement.prototype, "src", descriptor);

通过同样的办法,也能够把 imglinkiframeastyle 中和 URL 相干的属性解决掉。

提醒:innerHTML也是通过这种办法进行解决。

CSS 中的申请

要发动一个申请,除了下面形容的办法外,也能够通过 css 中的 background-image 属性发动。

document.getElementById("#id").style.background="url(/data/hook-before.jpg)";

CSS 属性属于 CSSStyleDeclaration 对象,该对象的原型上有以下属性能够发动申请:

  • cssText
  • background-image
  • background
  • border-image
  • borderImage
  • border-image-source
  • borderImageSource

应用代码示例中的办法设置 CSS 属性,会间接发动申请,咱们无奈拦挡。然而,咱们能够通过调用 CSSStyleDeclarationsetProperty办法进行属性设置,所以咱们须要在 CSSStyleDeclaration 的原型链上定义下面的属性,通过设置 settergetter,而后调用 setProperty 办法进行理论设置。代码示例如下:

    Object.defineProperty(CSSStyleDeclaration.prototype, "background",
        {get: function () {return this.getPropertyValue("background");
            },
            set: function (v) {v=srcHook(v);
                this.setProperty("background", v);
            }
        }
    );
    Object.defineProperty(CSSStyleDeclaration.prototype, "background-image",
        {get: function () {return this.getPropertyValue("background-image");
            },
            set: function (v) {v=srcHook(v);
                this.setProperty("background-image", v);
            }
        }
    );
    var descriptor = Object.getOwnPropertyDescriptor(CSSStyleDeclaration.prototype, "setProperty");
    var valuer = descriptor["value"];
    descriptor["value"] = function () {if (srcHook) {var src = srcHook(arguments[1]);
            if (src === false) return;
            if (src) {arguments[1] = src;
            }
        }
        return valuer.apply(this, arguments);
    }
    descriptor["configurable"] = false;
    // 因为 src 的 set 有可能会被其它脚本批改回去,此处通过设置 configurable=false 来强行禁止批改
    Object.defineProperty(CSSStyleDeclaration.prototype, "setProperty", descriptor);

因为在对 background-imagebackground 等属性进行 hook 时,调用了 setProperty 办法进行设置,若原代码中间接就调用的 setProperty 办法进行设置,则须要对 setProperty 的属性描述符 (property descriptor) 进行重写。

HTML 中的申请

HTML 中的申请,咱们无奈进行拦挡,但能够应用 MutationObserver 监听 DOM 对象的创立,对于其中的 a 标签,能够批改 href 属性。对于 imgsrc属性也能够批改,但无奈阻止申请的收回,批改后的申请也会失常收回。

咱们先在 HTML 中增加一个图片显示的 DOM

<img src="/data/hook-before.jpg" />

在没有监听和批改前,页面显示的是 HOOK 前的图片,如下:

而后,咱们在 JS 中增加监听和批改的代码,咱们仅用 IMG 进行测试:

function DomWatch() {
    // part 1
    var observer = new MutationObserver(function(mutationsList, mutationObserver){mutationsList.forEach(function(mutation){if(!mutation.addedNodes) return;
            mutation.addedNodes.forEach(function(node){if(node.tagName!=="IMG") return;
                node.src=srcHook(node.src);
            })
        })
    });
    // part 2
    observer.observe(document, {childList:true,attributes:true,subtree:true});
}
DomWatch();

保留,而后刷新一下页面,能够发现显示的图片曾经产生了扭转。

在这里,尽管咱们看到的图片曾经产生了变动,但理论是在 HTML 中指定的图片仍然会发出请求。

在 Developer Tools 的网络标签中,能够看到,收回了两次图片申请。

对于 MutationObserver 的具体用法,请能够参考

  • https://developer.mozilla.org/zh-CN/docs/Web/API/MutationObserver
  • https://www.zhangxinxu.com/wordpress/2019/08/js-dom-mutation-observer/

在 HTML 中的 DOM,也能够通过遍历的形式进行批改,然而如果用 innerHTML 创立的 DOM,解决上就会比拟麻烦。

WebSocket 中的申请

WebSocket 中的申请是在 new 的时候指定的,如下:

new WebSocket("ws://121.40.165.18:8800")

咱们须要拦挡 WebSocket 的 new 操作,并将连贯地址批改为咱们须要的地址,对于 new 的拦挡,这里应用 ES6 的 Proxy 进行解决。在这里,咱们对立将地址批改为ws://119.29.3.36:6700/

const __WebSocket = new Proxy(window.WebSocket, {construct(target, args) {args[0]="ws://119.29.3.36:6700/";
       return new target(...args);
   }
});
window.WebSocket = __WebSocket;

未解决的问题

如果在网页的脚本中,有通过 locationlocation.href来重定向页面地址,则无奈对这个动作进行拦挡,location对象曾经被浏览器定义为了不可伪造,目前没有找到好的方法,只能通过服务端代理,将调用该属性的 js 代码进行替换。

通过属性形容能够看到,window上的 locationlocation.href均设置了不可批改。

{enumerable: true, configurable: false, get: ƒ, set: ƒ}

写在最初

本文是以前在将淘宝手机页面搬进微信里显示的时候的钻研后果,但起初这个也没有用起来,当初基于互联互通的政策要求,也就更没有应用场景了。

本文的相干代码已上传:

  • https://gitee.com/zsea/jshook
  • https://github.com/zsea/jshook
正文完
 0