长亭百川云 - 文章详情

前端黑魔法 —— 隐藏网络请求的调用栈 - EtherDream

博客园 - EtherDream

44

2024-07-20

前言

浏览器网络控制台会记录每个请求的调用栈(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:0s linear 0s __a__;'
container.innerHTML =
  '<style>' +
    '@keyframes __a__{' +
      'from{font-size:0}' +
      'to{font-size:0}'
    '}' +
  '</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 timer = setTimeout(sendFn, 10)
const clearFn = clearTimeout.bind(window, timer)
container.addEventListener('animationend', clearFn)

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

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

同时注册了 3 个 animationend
事件,第 1 个用于发送请求;第 2 个用于删除元素,这样页面中不会有 DOM 残留;第 3 个用于删除定时器 —— 假如出现意外情况导致动画事件未触发,或未能及时触发,那么至少还有后备定时器会帮我们触发请求。

由于 IE 浏览器 CSS 的 @keyframes 里必须有规则,否则动画事件不会触发。出于兼容性,随便放一些规则即可。

Chrome:

FireFox:

Safari:

相关推荐
关注或联系我们
添加百川云公众号,移动管理云安全产品
咨询热线:
4000-327-707
百川公众号
百川公众号
百川云客服
百川云客服

Copyright ©2024 北京长亭科技有限公司
icon
京ICP备 2024055124号-2