November 24, 2025

Esbuild has been downloaded 5 billion times since this XSS bug was introduced in 2022. The bug hid in a function that promised to escape html called escapeForHTML. But, apparently, the promise was more of a suggestion. To bypass the HTML escaping, I used the quote of the day, literally a quote ". A malicious folder with a quote in its name could be used to attack anyone using the dev server. The fix was one line. The exploit involved making an invisible script take over your entire screen.
Buckle up.
This adventure kicked off with our depthfirst system tapping me on the shoulder like an overeager intern with a suspiciously confident smile.
Possible XSS? Sure.
Low severity? Ok.
html.WriteString(escapeForHTML( ... ))In esbuild? Using a function literally named escapeForHTML? Unlikely. I had the same reaction you’d have if someone told you a toaster was capable of launching a space shuttle: charming, but wrong.
Our system claimed there’s an XSS bug inside code designed to prevent XSS? In a major codebase built around generating safe HTML? If true, that’s like finding out the lifeguard can't swim.
Still, if valid, this would be a significant finding. The esbuilt npm package alone has five billion downloads. And a restless “but what if?” rang in the back of my mind. So I sighed, cracked my knuckles, and set out to prove the machine wrong. Spoiler: the machine was not wrong.
The depthfirst system had already labeled it “low severity,” which is our polite way of telling engineers, “not a fire, but this smells funny.”
But I couldn’t let it go. Even when a machine says “low severity,” I still want to understand why it thinks something is off. It’s like hearing your dog growl at a blank wall. Maybe it’s nothing, but maybe it’s time to call a priest.
So I followed the trail into esbuild’s code.
Here’s the vulnerable code :
At first, nothing seemed odd. The dev server is creating the h1 title from directory listings. It's escaping HTML in the folder names. All the classics get neutralized: <, >, &, '.
But one thing didn’t get escaped. Quotes ".
I have confirmed our system's finding and suddenly everything clicked into place. I gave my laptop a pat on the head to reward the AI.
escapeForHTML correctly protects you when you put user-controlled text between tags, like:
But esbuild wasn’t putting the escaped text there. It was putting it inside an HTML attribute, in an href:
If your sanitization doesn’t escape double quotes, you can break out of the attribute and add your own. You can slap on a new style, an event handler, or an entire circus of JavaScript!
The correct function to use was escapeForAttribute:
Once I realized I could break out of the attribute, the rest was pure puzzle-solving joy.
I needed a folder name that:
/" at the end
Here’s the command that created the malicious directory:
Let me unpack the magic:
style="position:absolute;top:0;left:0;width:100vw;height:100vh;"onmouseover="alert('xss')"data-x="Reload the dev server. Move your mouse. Instant satisfaction.
After confirming the exploit was real, I sent the automatically generated fix upstream. The patch was immediately merged.
The fix? Literally a swap:
One word. Billions of future downloads affected.
I love bugs like this. They're subtle, and make you think deeply about the edge cases of the code.
The maintainers thanked us for finding and fixing the bug, and was correct to point out this didn't have a security impact. Since this only affects the dev server, and the dev server assumes a trusted environment, it’s not a “security vulnerability” in the traditional sense. And that’s true. This wasn’t a CVE-worthy disaster. No one’s production servers were melting because of this.
But it was still a bug. An elusive, fun, intellectually stimulating bug that was completely exploitable.
And depthfirst’s system correctly found, categorized, and drafted a patch. All automatically.
I just got to be the human who enjoyed the ride.
This adventure felt like tugging on a loose thread in a sweater: you don’t expect much, but suddenly half the sleeve is in your hand. All I did was follow a quote mark out of an attribute, and it led to a bug that had been downloaded billions of times. The funny part is that nothing here was “wrong” in isolation. The trick was noticing the context had changed. escapeForHTML was perfectly fine for text, just not for attributes.
Depthfirst surfaced the loose thread; I pulled it because I can’t resist seeing where those threads lead. Together, we solved a tiny mystery tucked away in a project downloaded five billion times.

Secure your code to ship faster
Link your Github repo in three clicks.