Skip to content

The assumption that quietly went stale

A safe shortcut in my SEO code stopped being safe the day testimonials moved to the database. Nobody told the code.

SD
Shubham Datarkar
· 2 min read

The most dangerous line of code I wrote yesterday was one I wrote months ago and never touched again. It was correct when I wrote it. It quietly stopped being correct, and nothing told me.

I have a small component called JsonLd. Its whole job is to take structured data -- the machine-readable schema that helps Google understand a page -- and drop it into a script tag in the HTML. For a long time it did that the obvious way: JSON.stringify the object, inline the string, done. No escaping, no ceremony.

That was fine, and I could even justify it. All the schema data was mine. I wrote the titles, the descriptions, the org details, the FAQ answers. Author-controlled input. There is nothing to attack when you are the only one who can put words in.

Then testimonials moved to the database

At some point the /testimonials page started building its review schema from database rows. Each testimonial quote gets embedded, verbatim, into the schema as reviewBody. Perfectly reasonable feature. Except the quiet premise underneath JsonLd -- this data is author-controlled -- had just become false, and the code had no idea.

Here is the sharp edge. JSON.stringify does not escape the less-than character. So a quote containing the literal text that closes a script tag would close my script element early, and everything after it would parse as HTML. That is stored XSS. Gated behind admin write access, yes -- so not a five-alarm fire -- but a real one, sitting there, invisible, waiting for the wrong string.

What gets me is that no single commit introduced this. The JsonLd shortcut was safe. The testimonials feature was safe. The bug lived in the space between them, in an assumption that was true on Monday and false on Friday and never announced the change.

The fix is boring, which is the point

Escape every less-than character as a unicode escape sequence before inlining. That is still valid JSON, so anything reading the schema decodes it right back -- the meaning is identical. But the sequence that closes a script tag simply cannot appear in the output anymore. A few lines, plus a test that feeds it a hostile string and checks the tag survives.

Code does not rot because you change it. It rots because the world around it changes and the code keeps believing the old story.

Note to self, 2 a.m.

The lesson I am taking is not about escaping. It is about assumptions that are load-bearing but unwritten. Author-controlled was a real constraint holding that shortcut up, and it lived nowhere except my head. Today I am going hunting for the other ones -- the little because-I-said-so beliefs scattered through the code that no longer know they stopped being true.

Reactions

How was this article?

Newsletter

Never miss the next one

Get the latest playbooks, build logs, and the occasional unpublished idea straight to your inbox. One signal a week — no noise.