前言
瀏覽器網絡控制檯會記錄每個請求的調用棧(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: