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

AI Summary6 min read

TL;DR

A simple holiday snow effect script, copied from the internet and using innerHTML, becomes an XSS vulnerability when later modified to accept external input. This highlights how small UI features can pose security risks if not properly reviewed.

Key Takeaways

  • Using innerHTML with untrusted data can lead to XSS vulnerabilities, even in seemingly harmless code like visual effects.
  • Security risks increase when code crosses trust boundaries, such as fetching data from external sources like a CMS or API.
  • Modern frameworks like React or Angular only protect against XSS within their rendering systems; using innerHTML or similar methods bypasses these safeguards.
  • Prevent XSS by using safer alternatives like textContent, sanitizing HTML with libraries like DOMPurify, or creating DOM nodes explicitly.
  • Always conduct code reviews and security testing, even for small features, to avoid hidden vulnerabilities.

Tags

webdevjavascriptsecurityfrontend

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 ❄️✨

Visit Website