Luma event pages look simple on the surface: title, description, speaker bios, links, embeds, maybe a custom script or two. That simplicity is exactly why teams get sloppy with XSS. They assume “it’s just an event page,” then bolt on user-generated content, third-party widgets, and custom HTML until the page becomes a perfect little script execution engine.

I’ve seen this pattern a lot. Event pages tend to be built fast, owned by marketing or community teams, and touched by engineers only when something breaks. That’s where XSS sneaks in.

Here are the most common mistakes I see on Luma event pages, and the fixes that actually hold up.

Mistake #1: Treating event descriptions as trusted HTML

The classic bug: someone wants rich formatting in an event description, so the app stores HTML and injects it directly.

descriptionContainer.innerHTML = event.description;

If event.description contains this:

<img src=x onerror=alert(document.domain)>

you’ve got stored XSS.

This happens when organizers can edit event content, guest speakers submit bios, or an imported feed populates descriptions from another system. People hear “trusted organizer” and stop thinking about abuse. Bad call. Accounts get compromised. Integrations go weird. Internal users paste garbage from random tools.

Fix

If you only need text, render text.

descriptionContainer.textContent = event.description;

If you genuinely need rich HTML, sanitize it before rendering. Use a real sanitizer, not regex, not hand-rolled filters, not “remove <script> tags and call it a day.”

A solid client-side example with DOMPurify:

<script src="https://unpkg.com/[email protected]/dist/purify.min.js"></script>
<script>
  const clean = DOMPurify.sanitize(event.description, {
    ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'ul', 'ol', 'li', 'a'],
    ALLOWED_ATTR: ['href', 'title', 'target', 'rel']
  });

  descriptionContainer.innerHTML = clean;
</script>

Server-side sanitization is even better because it protects every consumer of that content, not just one frontend.

Mistake #2: Building speaker cards with template strings

This one is everywhere:

speakerList.innerHTML += `
  <div class="speaker">
    <h3>${speaker.name}</h3>
    <p>${speaker.bio}</p>
  </div>
`;

Looks harmless. It isn’t. If speaker.name or speaker.bio contains HTML, the browser parses it.

Fix

Create DOM nodes and assign text safely.

const card = document.createElement('div');
card.className = 'speaker';

const name = document.createElement('h3');
name.textContent = speaker.name;

const bio = document.createElement('p');
bio.textContent = speaker.bio;

card.append(name, bio);
speakerList.appendChild(card);

Yes, it’s more verbose than template strings. It’s also much harder to accidentally turn into an XSS sink.

If you use React, Vue, or Svelte, stick to their default escaped rendering. The danger starts when someone reaches for escape hatches like dangerouslySetInnerHTML, v-html, or raw HTML directives because “marketing needs formatting.”

Luma pages often include:

  • external registration links
  • speaker websites
  • sponsor URLs
  • community links
  • custom call-to-action buttons

A lot of code just drops those URLs straight into the DOM:

link.href = event.externalUrl;

That’s better than innerHTML, but it’s not enough if you don’t validate the scheme. A malicious value like this can still hurt:

javascript:alert(1)

Fix

Allow only expected protocols.

function safeUrl(input) {
  try {
    const url = new URL(input, window.location.origin);
    if (url.protocol === 'http:' || url.protocol === 'https:') {
      return url.href;
    }
  } catch (_) {}
  return null;
}

const href = safeUrl(event.externalUrl);
if (href) {
  link.href = href;
  link.rel = 'noopener noreferrer';
  link.target = '_blank';
}

Also validate URLs on the backend. Frontend checks are nice. Backend checks are mandatory.

Mistake #4: Allowing custom embed code from organizers

This is where event pages go from “a little risky” to “wide open.”

Some teams let organizers paste arbitrary embed snippets for maps, videos, sponsor widgets, countdown timers, or chat boxes:

<div class="custom-embed">
  {{ organizerEmbedCode }}
</div>

If that embed code is rendered as HTML, you’ve basically offered an XSS text box.

Fix

Don’t allow arbitrary embed HTML unless you absolutely have to. Prefer structured fields instead:

  • YouTube URL
  • Vimeo URL
  • Google Maps URL
  • approved iframe source list

Then generate the embed yourself.

function createYouTubeEmbed(videoId) {
  const iframe = document.createElement('iframe');
  iframe.width = '560';
  iframe.height = '315';
  iframe.src = `https://www.youtube.com/embed/${videoId}`;
  iframe.allowFullscreen = true;
  iframe.setAttribute('loading', 'lazy');
  iframe.setAttribute('referrerpolicy', 'strict-origin-when-cross-origin');
  return iframe;
}

If iframes are necessary, lock them down with sandbox where possible.

<iframe
  src="https://trusted.example/embed/123"
  sandbox="allow-scripts allow-same-origin"
  loading="lazy">
</iframe>

Even then, keep the source allowlist tight.

Mistake #5: Assuming Markdown is safe by default

A lot of event systems support Markdown for descriptions and bios. People treat it like a security feature. It isn’t.

Depending on the parser and configuration, Markdown may allow raw HTML:

Great event!

<script>alert(1)</script>

Or it may permit dangerous link payloads:

[Click me](javascript:alert(1))

Fix

Use a Markdown parser configured to disable raw HTML, then sanitize the rendered output anyway.

Example with marked plus DOMPurify:

import { marked } from 'marked';
import DOMPurify from 'dompurify';

marked.setOptions({
  headerIds: false,
  mangle: false
});

const dirtyHtml = marked.parse(markdownInput);
const cleanHtml = DOMPurify.sanitize(dirtyHtml);
container.innerHTML = cleanHtml;

Don’t trust the parser to solve this by itself. Parsers are for formatting. Sanitizers are for security.

Mistake #6: Ignoring DOM-based XSS in filters and previews

Luma-style event pages often have client-side features like:

  • live event preview
  • URL-driven filters
  • referral tags
  • search highlighting
  • “share this event” banners

That leads to code like this:

const params = new URLSearchParams(location.search);
banner.innerHTML = `Showing results for: ${params.get('tag')}`;

That’s DOM XSS from the query string.

Fix

Same rule as before: use textContent for untrusted data.

const params = new URLSearchParams(location.search);
banner.textContent = `Showing results for: ${params.get('tag') || 'all events'}`;

If you need markup around the value, separate structure from content.

label.textContent = 'Showing results for: ';
value.textContent = params.get('tag') || 'all events';
banner.append(label, value);

Mistake #7: Weak or missing CSP

I’m opinionated here: if you run user-editable event content and don’t have a Content Security Policy, you’re making life too easy for attackers.

CSP won’t fix bad rendering code. It will reduce the blast radius when someone misses a sink, and people always miss sinks.

A decent starting point:

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-rAnd0m123';
  object-src 'none';
  base-uri 'self';
  frame-ancestors 'self';
  img-src 'self' https: data:;
  style-src 'self' 'unsafe-inline';
  frame-src https://www.youtube.com https://player.vimeo.com;

If you can avoid inline styles too, do it. If you need CSP guidance beyond the basics, csp-guide.com is a useful reference.

Also, test your headers properly. I like using HeaderTest for a quick sanity check when I’m validating CSP and related security headers on public pages.

Mistake #8: Sanitizing once, then mutating later

This bug is sneaky. Teams sanitize HTML on input, store it, then later run “harmless” transformations that reintroduce risk.

Example:

const clean = sanitize(userHtml);
const linked = clean.replace(/#(\w+)/g, '<a href="/tags/$1">#$1</a>');
container.innerHTML = linked;

That replacement step can break assumptions fast, especially if the original content contains edge cases the sanitizer normalized but your string manipulation didn’t expect.

Fix

Treat HTML as parsed DOM, not string soup. If you need post-processing, do it on the DOM after sanitization, or process the raw text before rendering into HTML.

Better yet, store structured content instead of half-trusted HTML blobs.

Mistake #9: Forgetting that third-party widgets can become your problem

Analytics, scheduling tools, chat widgets, social embeds, sponsor scripts — event pages attract all of them. Every third-party script you add gets the same DOM access your own code has.

That’s not technically “XSS” in the strict stored/reflected sense, but from a user impact perspective, compromised third-party JS behaves pretty much the same.

Fix

Be ruthless about third-party JavaScript:

  • remove anything nonessential
  • self-host only when licensing and update practices make sense
  • pin versions
  • use Subresource Integrity where possible
  • isolate untrusted functionality in iframes
  • keep CSP tight

If marketing asks for five different trackers on a single event page, push back. You’re not being difficult. You’re doing your job.

A practical checklist for Luma event pages

If I were reviewing a Luma-style event feature, I’d check these first:

  • Any use of innerHTML, outerHTML, insertAdjacentHTML, or jQuery .html()
  • Markdown or rich text rendering paths
  • speaker bios, event descriptions, sponsor blurbs, and custom links
  • URL parameters reflected into the page
  • custom embed support
  • third-party script includes
  • CSP coverage and nonce handling
  • backend validation for URLs and content types

And I’d fuzz with payloads like:

<img src=x onerror=alert(1)>
<svg onload=alert(1)>
<a href="javascript:alert(1)">click</a>

If any of those survive into execution, the page is still soft.

XSS on event pages usually comes from convenience: “just render the HTML,” “just support embeds,” “just let organizers customize it.” That convenience gets expensive fast. The fix is boring, which is exactly why it works: escape by default, sanitize when HTML is unavoidable, validate URLs, isolate third-party content, and backstop everything with CSP.

That’s the stack I trust. Anything looser tends to break the moment real users touch it.