close

前言

如何確定 JavaScript 的 原生函數有沒有被重寫過呢?今日前端早讀課文章由奇虎 360@Tapir 翻譯分享,公號:奇舞精選授權。

正文從這開始~~

簡單講:如何確定 JavaScript 的 原生函數有沒有被重寫過呢?我們沒法做到,或者說判定結果的可信度並不會特別高。我們有很多方法可以檢查,但是無法保證萬無一失。

JavaScript 中的原生函數

在 JavaScript 中,「原生函數」(Native function) 是那些源代碼被編譯為原生機器碼的函數。我們可以在 JavaScript 標準內置對象 中找到原生函數(諸如eval(),parseInt()) ,或者在 瀏覽器 Web API 找到(諸如fetch(),localStorage.getItem())。

由於 JavaScript 的動態特性,開發者可以覆蓋瀏覽器暴露出的原生函數。這種技巧被我們稱作 猴子補丁(Monkey patching ) 。

猴子補丁

猴子補丁主要用於修改瀏覽器內置 API 和原生函數的默認行為。這通常是添加特定功能、polyfill 特性、hook 到 API 的唯一方法,因為我們沒法直接對這些 API 進行訪問。

例如,像是 Bugsnag 這樣的監測工具,重寫了 Fetch 和 XMLHttpRequest 的 API 來獲取由 JavaScript 代碼觸發的網絡連接相關信息。

猴子補丁是個強大而危險的技巧,因為你沒法控制那些被你覆蓋的代碼:未來 JavaScript 引擎的更新可能會打破你在補丁中做出的一些假設,並導致嚴重的 Bug。

另外,對那些並非由你負責的代碼打猴子補丁,可能會覆蓋一些被其他開發者加入的猴子補丁,引入潛在的衝突。

由於種種原因,有時需要確定給定函數是否是原生函數,是否被打了猴子補丁,但是我們能做到嗎?

用toString()來檢查函數上的猴子補丁

檢查一個函數是否 「乾淨」(沒有猴子補丁) 最常用的方式那就是檢查函數的toString()輸出。

默認情況下,原生函數的toString()返回這麼一行 "function fetch() { [native code] }"

依照運行 JavaScript 引擎的不同,輸出結果會略有不同。不過,在大多數瀏覽器中,還是可以很安全的假定返回的字符串中會包含 "[native code]" 。

打過猴子補丁的原生函數,它的toString()將不會返回包含 "[native code]" 的字符串,而是會返回字符串化的函數體。

所以說,想要知道函數是否仍是原生的,我們可以通過檢測toString()輸出是否包含 "[native code]" 來簡單判斷。

基本的檢測方式如下:

function isNativeFunction(f) { return f.toString().includes("[native code]"); } isNativeFunction(window.fetch); // → true // 對 fetch API 打猴子補丁 (function () { const { fetch: originalFetch } = window; window.fetch = function fetch(...ƒargs) { console.log("Fetch call intercepted:", ...args); return originalFetch(...args); }; })(); window.fetch.toString(); // → "function fetch(...args) {\n console.log("Fetch... isNativeFunction(window.fetch); // → false

這種方式在大多數場景下都能正常生效。然而,你得清楚,很多伎倆可以讓函數繞過這個檢測。無論是出於惡意目的(注入惡意代碼)還是說你不希望自己的覆蓋行為被發現,有幾種方法可以讓函數看起來很 「原生」。

比如,可以添加一些包含 "[native code]" 的代碼(甚至是一條注釋!)在函數體裡:

(function () { const { fetch: originalFetch } = window; window.fetch = function fetch(...args) { // function fetch() { [native code] } console.log("Fetch call intercepted:", ...args); return originalFetch(...args); }; })(); window.fetch.toString(); // → "function fetch(...args) {\n // function fetch... isNativeFunction(window.fetch); // → true

… 或者,可以重寫toString()方法,返回包含 "[native code]" 的字符串:

(function () { const { fetch: originalFetch } = window; window.fetch = function fetch(...args) { console.log("Fetch call intercepted:", ...args); return originalFetch(...args); }; })(); window.fetch.toString = function toString() { return `function fetch() { [native code] }`; }; window.fetch.toString(); // → "function fetch() { [native code] }" isNativeFunction(window.fetch); // → true

… 或者,可以用 bind 創建猴子補丁函數,這會生成一個原生函數:

(function () { const { fetch: originalFetch } = window; window.fetch = function fetch(...args) { console.log("Fetch call intercepted:", ...args); return originalFetch(...args); }.bind(window.fetch); // })(); window.fetch.toString(); // → "function fetch() { [native code] }" isNativeFunction(window.fetch); // → true

… 或者,可以通過 ES6 的 Proxy 來捕獲apply()調用,這樣一來,從外部來看,函數完全是原生的:

window.fetch = new Proxy(window.fetch, { apply: function (target, thisArg, argumentsList) { console.log("Fetch call intercepted:", ...argumentsList); Reflect.apply(...arguments); }, }); window.fetch.toString(); // → "function fetch() { [native code] }" isNativeFunction(window.fetch); // → true

好了,我就不舉例子了。

我主要想強調的是:開發者可以輕易地繞開你的toString()檢測。

我覺得大多數情況下,不需要太在意上面那些邊緣情況。但是如果你想的話,還是可以用一些額外檢測來覆蓋上面的用例。

例如:- 可以使用一次性的 iframe 來獲取 「乾淨」 的toString()值,再做嚴格匹配;- 可以多次調用.toString().toString()確保toString()不被重寫;- 使用元編程技巧,對 Proxy 構造函數自身來打個猴子補丁,以此來確定原生函數是否被代理過了(因為依照規範,無法察覺到什麼東西是 Proxy) - 等等 …

這完全取決於你想在toString()這個兔子洞裡鑽多深。

但是這真的值得嗎?我們能夠覆蓋所有的邊緣情況嗎?

從 iframe 獲取乾淨的函數

如果你需要調用一個 「乾淨」 的函數,而不是去檢查原生函數是不是被打過猴子補丁,那麼我們可以從同源的 iframe 中獲取:

// 創建一個同源的 iframe // 你可能需要添加一些樣式先隱藏 iframe,稍後再從 DOM 中徹底刪除 const iframe = document.createElement("iframe"); document.body.appendChild(iframe); // 新的 iframe 會創建它自身的 「乾淨」 window 對象,這樣你就可以從這裡拿到你想要的函數了 const cleanFetch = iframe.contentWindow.fetch;

儘管,我覺得這種方式比調用toString()去做驗證要好,但也會有一些局限性;- iframe 有時會由於 強 CSP 或者 你的代碼沒有通過瀏覽器運行 而導致不可用。- 儘管不太現實,但第三方可以給 iframe API 上猴子補丁。所以還是不能 100% 信任生成 iframe 的 window 對象。- 修改或調用 DOM 的原生函數(比如document.createElement)沒法使用這種方法,因為它們會指向 iframe 的 DOM 而不是頂層的 DOM。

這個解決方案來自 https://lobste.rs/s/pppun8/checking_if_javascript_native_function 。

通過判斷引用是否相等來檢查函數上的猴子補丁

如果安全是你首要考慮的因素,我認為你可以選擇一種不同的方法:長期存儲一個 「乾淨」 的原生函數引用,然後,用它來和可能的猴子補丁函數進行比較:

<html> <head> <script> // 在其他腳本修改原生函數之前,保存 「乾淨」 原生函數的原始引用。 // 在這個例子中,我們保存了 fetch API 的原始引用 // 並把它保存在閉包里。如果你無法預先決定要檢查什麼 API, // 那可以存儲多個 window 對象。 (function () { const { fetch: originalFetch } = window; window.__isFetchMonkeyPatched = function () { return window.fetch !== originalFetch; }; })(); // 現在開始,你可以調用 window.__isFetchMonkeyPatched() // 來檢查 fetch API 是不是被打了猴子補丁 // // 例如: window.fetch = new Proxy(window.fetch, { apply: function (target, thisArg, argumentsList) { console.log("Fetch call intercepted:", ...argumentsList); Reflect.apply(...arguments); }, }); window.__isFetchMonkeyPatched(); // → true </script> </head> </html>

通過嚴格的引用檢查,我們可以避免所有的 toString () 漏洞。甚至這種方式也能應用於 Proxy,因為 Proxy 沒法捕獲相等性比較 。

這種方法最大的問題在於有點不切實際。它需要在運行任何 app 中其他代碼之前,保存函數的原始引用,以確保函數沒有被動過手腳。但我們有時根本沒法做到這一點(比如,你構建的是庫)。

可能有些方式能繞過這項測試,但是我在撰寫本文時沒有想到。歡迎大家補充。

那麼,如何確定 JavaScript 原生函數是否被重寫過呢?

我 需要 檢查函數上猴子補丁的次數,用一隻手都能數得過來。

不過我對這個問題很感興趣,我認為對於很多場景,不存在真正萬無一失的判定方法。

如果你能控制整個網頁,可以預先在函數都還是 「乾淨」 的時候存儲它們,之後再進行比較。

不然,你可以使用 iframe,創建一次性的 iframe 並從中獲取 「乾淨」 的函數。但你要明白你還是無法 100% 確定 iframe API 是否被動了手腳。

再者,由於 JavaScript 的動態特性,你可以簡單使用toString().includes("[native code])"來檢查(但惡意代碼很容易繞過這種檢測)。你還可以增加大量的安全檢測來覆蓋大多數(沒法做到全部)的邊緣情況。

關於本文譯者:@Tapir譯文:https://zhuanlan.zhihu.com/p/564194625作者:@Mazzarolo Matteo原文:https://mmazzarolo.com/blog/2022-07-30-checking-if-a-javascript-native-function-was-monkey-patched/

關於【原生】相關推薦,歡迎讀者自薦投稿,前端早讀課等你來

【第2555期】如何製作一個媲美原生體驗的選擇器組件【第1045期】原生 JavaScript 值得學習嗎?答案是肯定的

arrow
arrow
    全站熱搜
    創作者介紹
    創作者 鑽石舞台 的頭像
    鑽石舞台

    鑽石舞台

    鑽石舞台 發表在 痞客邦 留言(0) 人氣()