DEV Community

Cover image for ❄️A Five-Minute UI Feature That Became an XSS Time Bomb
Sylwia Laskowska
Sylwia Laskowska

Posted on

❄️A Five-Minute UI Feature That Became an XSS Time Bomb

Can a simple script — a trivial visual effect — put your application at risk?
Oh yes. And you might not even realize how.

What’s more, small, innocent-looking pieces of code can turn into shiny, colorful time bombs. How is that possible?

Let me tell you a hypothetical story.


❄️ The Snow Begins to Fall

Imagine you develop a website, a shop, or a web application.
December comes around. Lights, trees, decorations everywhere. The holiday mood starts to get to you.

You — or one of your stakeholders — asks for a small seasonal touch.
“Maybe some falling snow?” ❄️

You’re excited and immediately jump on this very creative task.

But wait.

The backlog is overflowing. Deadlines are screaming. And suddenly you remember something important:
you’re lazy. 😉

You’re obviously not going to write this from scratch.

So you do what all of us do:
you open CodePen, GitHub, maybe Stack Overflow… and copy a random snippet.


❄️ The Innocent Snow Script

<script>
  /**
   * ❄️ Simple snow effect
   * Source: random blog / CodePen
   */
  const snowflakes = ["❄️", "", ""];

  for (let i = 0; i < 40; i++) {
    const el = document.createElement("div");
    el.className = "snowflake";

    // ❌ copied straight from the internet
    el.innerHTML =
      snowflakes[Math.floor(Math.random() * snowflakes.length)];

    el.style.left = Math.random() * 100 + "vw";
    el.style.fontSize = 12 + Math.random() * 24 + "px";
    el.style.animationDuration = 5 + Math.random() * 5 + "s";

    document.body.appendChild(el);
  }
</script>
Enter fullscreen mode Exit fullscreen mode

Five minutes later — boom — you have a beautiful falling snow effect.

Falling JavaScript snow and heading

You don’t even put it up for a proper code review.
Or someone glances at it briefly, because hey — it’s just a visual effect, right?

Security tests?
Why would anyone test falling snow?


🎁 Congratulations — You’ve Just Shipped an XSS

The problem is right here:

el.innerHTML = snowflake;
Enter fullscreen mode Exit fullscreen mode

XSS (Cross-Site Scripting) happens when untrusted data is injected into the DOM in a way that allows it to be executed as HTML or JavaScript.

Important clarification: at this exact moment, this code is not yet an active XSS vulnerability, because snowflake comes from a hard-coded, fully trusted array.
However, the dangerous pattern is already in place — and that’s what turns this into a time bomb.

For now, nothing bad happens.
Everything looks fine.


⏳ Time Passes…

January comes. Management decides it’s time to turn the visual effect off.

“But don’t remove it!”
“We’ll need it next year!”

Or maybe even earlier — spring is coming, so instead of snowflakes, let flower petals fall 🌸.

A new task lands in the backlog.
Another developer picks it up and thinks:

“I’m not going to toggle this every few months.
Let management control it themselves — and maybe even choose the icons.”

So they add configuration.
Maybe via a CMS. Maybe via a remote endpoint.


🌐 The “Small Improvement”

function fetchSeasonalConfig() {
  return Promise.resolve({
    enabled: true,
    snowflake: "❄️"
  });
}

fetchSeasonalConfig().then(config => {
  if (!config.enabled) return;

  for (let i = 0; i < 30; i++) {
    const el = document.createElement("div");
    el.className = "snowflake";

    // ❌ still innerHTML
    el.innerHTML = config.snowflake;

    document.body.appendChild(el);
  }
});
Enter fullscreen mode Exit fullscreen mode

Now the doors are wide open.

At this point, the value crosses a trust boundary: it no longer comes from a constant defined in code, but from an external source that can change independently of the application logic.

An attacker doesn’t need anything fancy:

  • a compromised CMS account
  • a misconfigured role
  • a WYSIWYG editor
  • a copied snippet from Notion or email
  • an intercepted or modified API response

For example:

snowflake: '<img src=x onerror="alert(\'XSS 🎄\')">'
Enter fullscreen mode Exit fullscreen mode

And that’s it.
Full XSS in your app.


🤔 “But I Use a Modern Framework!”

Some of you might be thinking now:

“Come on. This applies to old jQuery sites.
I use a modern framework — React / Angular / Vue — and it protects me from XSS.”

Nothing could be further from the truth.


⚠️ Frameworks Only Protect What They Render

React and Angular do escape content by default — but only inside their rendering system.

The moment you use:

  • innerHTML
  • dangerouslySetInnerHTML (React)
  • [innerHTML] or bypassSecurityTrustHtml (Angular)
  • or a plain JS script running outside the framework

…you’re on your own.

And guess what?

That snow script?
It often lives outside the framework, in index.html, loaded as a “small visual effect”.

Frameworks don’t sandbox random JavaScript files.


✅ How This Could Have Gone Differently

All of this could have been avoided with one simple change:

el.textContent = snowflake;
Enter fullscreen mode Exit fullscreen mode

Or by:

  • creating DOM nodes explicitly instead of injecting HTML
  • sanitizing HTML with a well-maintained library like DOMPurify (properly configured, with a strict allowlist)
  • clearly defining a security boundary: everything external is untrusted
  • treating “small UI features” with the same security mindset as core functionality

Defense-in-depth measures like Content Security Policy (CSP) can also reduce the impact — but they don’t fix unsafe DOM APIs.


🎄 Final Thoughts

Did this exact story happen?
No 😉

Have I heard dozens of very similar ones?
Absolutely.

Remember: no feature is too small to skip proper code review and security testing.

The devil is in the details.


🎁 Happy Holidays

Wishing you happy and peaceful holidays —
the kind you can give yourself by being just a little more careful about what you copy from the internet ❄️✨

Top comments (17)

Collapse
 
pascal_cescato_692b7a8a20 profile image
Pascal CESCATO

Even if you’re describing a fictional case, you’re absolutely right to call out this kind of vulnerability. We’ve seen the exact same pattern play out in real incidents — from the Samy worm on MySpace, to XSS worms on Twitter, and later on at companies like British Airways, GitHub, or Yahoo Mail. In most cases it started with small, seemingly harmless JavaScript that wasn’t designed with untrusted data in mind. So while the story is fictional, the risk it highlights is very real.

Collapse
 
sylwia-lask profile image
Sylwia Laskowska

Exactly! Thanks for calling this out, Pascal.

Those are very well-known cases, and the scary part is that they all started with something small and seemingly harmless. On top of that, there are countless similar incidents at smaller companies that never made the headlines, but still caused real damage.

I’ve seen this pattern many times myself: innerHTML used, safeguards consciously bypassed just to render something “nicely”. Awareness really matters here - these bugs are very real and can seriously hurt a business. The last thing anyone needs is a security incident on production.

Collapse
 
adamthedeveloper profile image
Adam - The Developer

dangerouslySetInnerHTML is basically:
“I acknowledge the risks, waive my rights, and accept XSS into my life.” 🤣

Collapse
 
sylwia-lask profile image
Sylwia Laskowska

I’m literally crying laughing 😂
That name is perfect: “you’ve been warned, this is on you now”.

I mostly work with Angular on a daily basis, and innerHTML is at least sanitized by default. But if you really want to shoot yourself in the foot, you can always inject DomSanitizer and use bypassSecurityTrustHtml 🙃
More annoying to get around, but hey - where there’s a will, there’s a way 😄

Thanks for this, made my day! ❄️

Collapse
 
sloan profile image
Sloan the DEV Moderator

We loved your post so we shared it on social.

Keep up the great work!

Collapse
 
sylwia-lask profile image
Sylwia Laskowska

Aww, thank you so much! ❤️
I swear, writing this post was at least as much fun for me as it seems to be for the people reading it 😄
Really appreciate the love and the share! ❄️

Collapse
 
htho profile image
Hauke T.

Thank you once again.

Yes - innerHtml is nasty.

The point is, that you should only use innerHtml (or insertAdjacentHtml) if you absolutely need it. If you don't need it to be parsed you should prefer innerText (insertAdjacentText). If not for security reasons, then at least for performance reasons.

Collapse
 
sylwia-lask profile image
Sylwia Laskowska

Thanks for the comment - totally agree 👍

InnerText / textContent (or insertAdjacentText) are almost always the better choice - for security and performance reasons.

That’s exactly why I wanted to share this story. Unfortunately, this is still a pretty common mistake, especially among junior developers - often copied straight from random snippets online.

Collapse
 
spo0q profile image
spO0q • Edited

nice!

Always set a robust CSP (content security policy).

Even if it can be a huge pain, it mitigates those untrusted injections.

Collapse
 
sylwia-lask profile image
Sylwia Laskowska

Absolutely agree 👍

A solid CSP is a huge pain sometimes, but it’s also one of the most effective safety nets you can have. It won’t fix bad patterns, but it does dramatically reduce the blast radius when something slips through - and something always does eventually.

Defense in depth for the win 😄
Thanks for calling this out!

Collapse
 
evan-lausier profile image
Evan Lausier

Very nice! Happy Holidays

Collapse
 
sylwia-lask profile image
Sylwia Laskowska

Thank you so much! 😊
Happy Holidays to you too! ❄️🎄

Collapse
 
shemith_mohanan_6361bb8a2 profile image
shemith mohanan

This is a fantastic cautionary story 👏
Love how you show how a harmless UI tweak slowly crosses trust boundaries and turns into real XSS — that progression is very real in production teams. The takeaway about innerHTML patterns, framework false-security, and “small features” skipping review is spot on. Clear, memorable, and genuinely educational.

Collapse
 
sylwia-lask profile image
Sylwia Laskowska

Aww, thank you so much - that really means a lot! 😊

I’m glad the progression resonated with you, because that’s exactly the part I wanted to highlight: how something harmless slowly crosses trust boundaries in very normal, very human ways. If it made the lesson feel clear and memorable, then the post did its job.

Thanks again for the kind words and for taking the time to write such a thoughtful comment! ❄️

Collapse
 
lfariaus profile image
Luis Faria

That was such an interesting post. Curious how such a small change could let doors wide open for scaling trouble!!!!

Collapse
 
sylwia-lask profile image
Sylwia Laskowska

Thank you, I’m really glad you found it interesting! 😊

That’s exactly the scary part - the change itself is tiny, but once it crosses a trust boundary, it can quietly scale into a much bigger problem. Especially in growing teams, those “small” shortcuts tend to get reused, extended, and copied until they’re everywhere.

Some comments may only be visible to logged-in visitors. Sign in to view all comments.