267 places in the codebase write a string into a <script> block or an inline handler, and we had to decide which of them were dangerous. The bug class is simple: if customer text contains </script>, the browser ends the script block right there and treats whatever follows as markup. An organization admin pastes an embed snippet into a page field, and the snippet breaks out.

A grep, then one question per hit

The first pass was a grep across the legacy application for every place that emits into a script context. Fixing every hit by hand would have produced a huge diff that nobody could review, and most of it would have been wrong anyway.

So we sorted each hit by one question: can a customer-typed value reach this string? Most sites emit constants, IDs, or values we generate ourselves. We followed each remaining hit back to its source and found 33 that carry customer input, meaning text a staff user or customer typed into a field that later renders on a public page. We fixed those 33 with explicit escaping at the call site. That part shipped first, and it is the boring, reviewable part.

Patched call sites only cover the ones you found

Thirty-three patched call sites protect us against the 33 we found. The 34th is written next spring by someone who has never heard of this audit, and it reintroduces the hole with no warning. Page render is the one place every page passes through on its way out, so that is where the permanent fix belongs. The rule at that choke point is that a closing script tag in dynamic output gets escaped before the page leaves the server. Nobody has to remember anything.

The first version, and what it broke

Our first version was the obvious one. At the render choke point, take the finished output and replace every </script with the escaped form <\/script. One rule covers every page and every future developer.

It passed the checks we had written for it, and we would have merged it. Then we drove it in a real session instead of trusting the unit tests, clicking through the actual admin flows with a browser attached. Image uploads broke.

The cause was that a global replace on finished output cannot tell a customer’s </script> from ours. The site has script-based content blocks of its own, and they legitimately emit closing script tags. Our rewrite reached into those and mangled them. The upload flow depends on one, so the block failed to close cleanly and the page stopped behaving.

The wrong turn cost real time. We had built the fix, written tests that confirmed it worked, and felt done. The tests only proved the escape happened. They said nothing about whether it happened to things it should have left alone, and that gap only showed up in a live session. Had we merged it, image uploads would have died silently for customers on a Tuesday release, with no hint that a security change was responsible.

The gated version

The version that merged makes one distinction. Output that came from customer-controlled values gets escaped. Output emitted by our own script-based content blocks passes through untouched. The escaping applies to the untrusted side of the boundary, so a </script> in a customer’s pasted snippet is neutralized while the site’s own blocks keep working.

We gated the merge on repeating the same browser session against it: the upload flow, the pages that use our script blocks, and a page with a hostile snippet pasted into a field. All three behaved. The ticket landed on develop, set to go live with the next Tuesday release.

Count first. Narrowing the list turned the audit from “fix everything” into a review a person could finish. Then move the guarantee to the choke point, because a fix that depends on people remembering will eventually fail, and the central rule means a 34th site written later is still covered.

The cost of the wrong turn is narrower. A choke-point filter sees everything, including things you did not mean to catch. Before it merges, run the real flows that pass through it and look at what it changed on the way out. Our tests told us the escape worked, and the browser session told us what else it hit.