前端黑魔法 —— 隱藏網絡請求的調用棧

前言

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章