前端黑魔法 —— 隐藏网络请求的调用栈 - EtherDream
2023-8-6 13:31:0 Author: www.cnblogs.com(查看原文) 阅读量:28 收藏

浏览器网络控制台会记录每个请求的调用栈(Initiator/启动器),可协助调试者定位到发起请求的代码位置。

为了不让破解者轻易分析程序,能否隐藏请求的调用栈?

事实上,使用之前 《如何让 JS 代码不可断点》 文中的方案,通过「内置回调」到「原生函数」,即可隐藏请求的调用栈:

const fn = fetch.bind(window, '/?by-click')
window.addEventListener('click', fn, {once: true})

点击页面任意位置即可产生网络请求,并且 Initiator 显示为 Other:

由于该方案依赖事件,因此得使用极易触发的事件,例如 mousemove。但用户一动不动的话,程序流程就会卡在这里。

如果直接用 JS 驱动该事件,例如:

const fn = fetch.bind(window, '/?by-dispatch')
window.addEventListener('click', fn, {once: true})

window.dispatchEvent(new Event('click'))

那么调用栈会原形毕露:

类似的,人为制造一个 error 事件:

const img = new Image()
img.onerror = fetch.bind(window, '/?by-onerror')
img.src = ''

或者人为产生一个 message 事件:

const fn = fetch.bind(window, '/?by-message')
window.addEventListener('message', fn, {once: true})
window.postMessage('')

这些都会暴露调用栈。

我们回顾下浏览器中总共有哪些事件:

https://developer.mozilla.org/en-US/docs/Web/API/Event#interfaces_based_on_event

事实上最容易触发的事件就在第一个:AnimationEvent。因为 CSS 动画播放时间是可控的,所以我们可创建一个 0 秒的动画,从而立即触发 animationend 事件:

<style>
  @keyframes k {}
  #el {
    animation-duration: 0;
    animation-name: k;
  }
</style>
<div id="el">Hello</div>
<script>
  const fn = fetch.bind(window, '/?by-animation')
  el.addEventListener('animationend', fn)
</script>

并且能完美隐藏调用栈:

除了动画事件,CSS 中的过渡事件 TransitionEvent 也能实现同样的效果。

const el = document.createElement('div')
el.style = 'transition: font 1s;'
document.body.appendChild(el)

const fn = fetch.bind(window, '/?by-transition')
document.body.addEventListener('transitionrun', fn)

requestAnimationFrame(() => {
  el.style.fontSize = '0'
})

相比动画事件需创建一个 @keyframes CSS 规则,过渡事件直接使用元素自身 style 属性即可,因此更简单。

不过需注意的是,动态添加的元素立即修改 CSS 是无法产生过渡事件的,需稍作延时才能修改。MDN 文档 也提到这点。

考虑到「过渡事件」需推迟执行并多一个回调,这里选择「动画事件」的方案,封装成纯 JS 实现:

const xhr = new XMLHttpRequest()
xhr.onload = () => {
  console.log('xhr onload')
}
xhr.open('GET', '/privacy')

const container = document.createElement('div')
container.style = 'animation-duration:0; animation-name:__a__;'
container.innerHTML = '<style>@keyframes __a__{}</style>'
document.body.appendChild(container)

const sendFn = xhr.send.bind(xhr)
container.addEventListener('animationend', sendFn)

const removeFn = container.remove.bind(container)
container.addEventListener('animationend', removeFn)

// 用于参照对比
const xhr2 = new XMLHttpRequest()
xhr2.open('GET', '/public')
xhr2.send()

为了请求后续操作,这里使用 XHR 取代 fetch,方便回调和获取数据。

同时注册了两个 animationend 事件,一个发送请求,另一个删除元素,这样页面中不会有 DOM 残留。

Chrome:

FireFox:

Safari:


文章来源: https://www.cnblogs.com/index-html/p/hide-request-initiator.html
如有侵权请联系:admin#unsafe.sh