浏览器网络控制台会记录每个请求的调用栈(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: