Tito is great for event registration, but I’ve seen teams treat embedded registration flows like a trusted UI just because it comes from a reputable platform. That’s where XSS bugs creep in. The weak spots usually aren’t Tito itself. They’re the custom wrappers, post-registration pages, attendee dashboards, and little bits of JavaScript glued around the form.

If you’re collecting attendee names, company names, dietary notes, discount codes, or custom answers and then displaying them anywhere in your app, you have an XSS surface. Tito registration data is user input. Treat it like any other untrusted data.

Here are the mistakes I see most often, and how to fix them.

Mistake #1: Trusting attendee fields because they came from Tito

A lot of developers assume data from a registration platform is “clean enough.” It isn’t. If an attendee enters this as their company name:

<img src=x onerror=alert('xss')>

and your app later renders it into an admin page, confirmation page, or attendee list with unsafe HTML insertion, you’ve got a stored XSS bug.

Bad

const attendee = await getAttendeeFromWebhook(req.body);

document.querySelector('#company').innerHTML = attendee.company_name;

Or on the server:

res.send(`
  <div class="attendee-company">${attendee.company_name}</div>
`);

Fix

Render untrusted data as text, not HTML.

document.querySelector('#company').textContent = attendee.company_name;

If you’re using server-side templates, rely on the template engine’s escaping, and do not disable it casually.

<div class="attendee-company"><%= attendee.company_name %></div>

Not this:

<div class="attendee-company"><%- attendee.company_name %></div>

The rule is simple: Tito registration fields are untrusted forever. They don’t become safe because they passed through a registration workflow.

Mistake #2: Using innerHTML for confirmation and summary screens

Teams often build a “registration summary” component that shows answers back to the user before or after checkout. That’s usually where innerHTML sneaks in because it feels convenient.

Bad

summary.innerHTML = `
  <h2>Thanks for registering, ${data.name}</h2>
  <p>Dietary requirements: ${data.dietary_requirements}</p>
`;

If either field contains HTML or script-capable payloads, you just executed attacker-controlled markup.

Fix

Build DOM nodes safely.

const heading = document.createElement('h2');
heading.textContent = `Thanks for registering, ${data.name}`;

const dietary = document.createElement('p');
dietary.textContent = `Dietary requirements: ${data.dietary_requirements}`;

summary.replaceChildren(heading, dietary);

If you absolutely must render limited HTML, sanitize it with a well-maintained HTML sanitizer and keep the allowed tag list tiny. Most of the time, registration data does not need HTML at all.

Mistake #3: Failing to encode data in the right output context

XSS prevention is not one generic “escape everything” step. The correct fix depends on where the data lands: HTML body, attribute, JavaScript string, URL, CSS, or inline JSON.

I still see developers safely escape data for HTML text and then reuse it inside an attribute or script block. That’s how weird edge-case bugs survive code review.

Bad

<div data-attendee="{{ attendee.name }}"></div>

If your templating setup does not correctly escape quotes for attributes, a payload can break out.

Even worse:

<script>
  const attendeeName = "{{ attendee.name }}";
</script>

That is a classic footgun.

Fix

Use context-aware escaping from your framework or template engine. Better yet, avoid inline script injection entirely.

Safer pattern:

<div id="attendee"
     data-name="{{ attendee.name | escape }}"></div>

Then read it in JavaScript:

const el = document.getElementById('attendee');
const attendeeName = el.dataset.name;

For larger structured data, serialize as JSON in a non-executable script block if your framework supports safe JSON helpers.

<script type="application/json" id="attendee-data">
  {"name":"Alice"}
</script>

Then parse it:

const raw = document.getElementById('attendee-data').textContent;
const attendee = JSON.parse(raw);

The exact helper depends on your stack, but the principle doesn’t: encode for the output context you’re actually using.

Mistake #4: Reflecting Tito query params into the page

A lot of Tito integrations support prefilled registration data through query parameters or custom flow state. Developers often read those values from location.search and write them into the DOM to personalize the page.

Bad

const params = new URLSearchParams(location.search);
document.getElementById('welcome').innerHTML =
  `Registering ${params.get('email')} for the event`;

That’s reflected XSS waiting to happen.

Fix

Use safe text rendering.

const params = new URLSearchParams(location.search);
document.getElementById('welcome').textContent =
  `Registering ${params.get('email') || 'your account'} for the event`;

And if you accept only expected values, validate them early.

const email = params.get('email');
const safeEmail = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email || '') ? email : '';

Validation is not a replacement for output encoding, but it reduces your attack surface and keeps garbage out of logs and UI.

Mistake #5: Building custom webhook dashboards without escaping

This is one of the most common real-world stored XSS paths. Tito sends registration data to your webhook, you persist it, then your support or events team views it in an internal dashboard. Internal dashboards are usually less polished from a security perspective, which makes them ideal XSS targets.

Bad

app.post('/tito/webhook', async (req, res) => {
  await db.attendees.insert(req.body);
  res.sendStatus(204);
});

app.get('/admin/attendees', async (req, res) => {
  const attendees = await db.attendees.findAll();

  const rows = attendees.map(a => `
    <tr>
      <td>${a.name}</td>
      <td>${a.company}</td>
      <td>${a.answers?.join(', ')}</td>
    </tr>
  `).join('');

  res.send(`<table>${rows}</table>`);
});

Fix

Escape on output, every time.

import escapeHtml from 'escape-html';

app.get('/admin/attendees', async (req, res) => {
  const attendees = await db.attendees.findAll();

  const rows = attendees.map(a => `
    <tr>
      <td>${escapeHtml(a.name || '')}</td>
      <td>${escapeHtml(a.company || '')}</td>
      <td>${escapeHtml((a.answers || []).join(', '))}</td>
    </tr>
  `).join('');

  res.send(`<table>${rows}</table>`);
});

If you’re using React, Vue, Django, Rails, Laravel, or any modern framework, lean on the built-in escaping defaults. The dangerous move is bypassing them for convenience.

Mistake #6: Allowing rich text in “special requests” fields without a sanitizer

I’ve seen event teams ask for “anything else we should know?” and then decide they want line breaks, links, or formatting when displaying those notes later. That usually turns a plain text field into an HTML field by accident.

Bad

notesContainer.innerHTML = attendee.special_requests;

Fix

Keep it plain text if you can.

notesContainer.textContent = attendee.special_requests;

If business requirements force rich text, sanitize before rendering and define a strict allowlist. No event registration note needs script, style, inline event handlers, or arbitrary URLs.

Also be careful with markdown renderers. Unsafe markdown pipelines are still XSS.

Mistake #7: Skipping CSP because “we already escape output”

You should escape output. You should also deploy a Content Security Policy. CSP won’t fix broken rendering logic, but it does make many XSS payloads much harder to exploit.

A weak CSP is common on registration pages because teams copy old snippets full of inline scripts and broad allowances.

Bad

Content-Security-Policy: default-src * 'unsafe-inline' 'unsafe-eval' data: blob:;

That’s barely a policy.

Better

Start with something like this and adapt it to your actual Tito embed and asset needs:

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-r4nd0m';
  style-src 'self' 'unsafe-inline';
  img-src 'self' data: https:;
  connect-src 'self' https:;
  frame-ancestors 'none';
  base-uri 'self';
  object-src 'none';

If you need implementation guidance, the practical reference I recommend is CSP Guide. For browser behavior and directives, use the official docs on MDN Content Security Policy.

The biggest CSP mistake I see is enabling 'unsafe-inline' for scripts just to get things working quickly. That removes much of the protection you wanted in the first place.

Mistake #8: Sanitizing input instead of fixing output handling

Developers love to “clean” registration input at ingestion time:

const cleaned = req.body.name.replace(/<.*?>/g, '');

This is fragile and usually breaks legitimate content while still missing payloads. You can do normalization and validation at input time, sure. But XSS is mostly an output problem.

Better approach

  • Validate format where it makes sense
  • Store the original value if you need fidelity
  • Encode or sanitize at render time based on context
  • Avoid dangerous sinks like innerHTML, outerHTML, insertAdjacentHTML, and inline script construction

A safer mental model for Tito registration

When I review Tito-based event flows, I use one blunt rule:

Every attendee-controlled field is hostile until the moment it is safely rendered for a specific context.

That includes:

  • Name
  • Email
  • Company
  • Job title
  • Accessibility notes
  • Dietary requirements
  • Custom answers
  • Discount code values
  • Query params used to prefill forms
  • Webhook payload fields
  • Admin notes copied from external systems

If your code touches any of those with innerHTML, template raw output, inline JavaScript interpolation, or unsafe markdown/HTML rendering, assume there’s an XSS bug until proven otherwise.

Practical checklist

If you want the short version, use this:

  • Use textContent instead of innerHTML
  • Keep template auto-escaping enabled
  • Never inject untrusted data into inline scripts
  • Validate query params and registration fields for expected formats
  • Sanitize only when you intentionally allow limited HTML
  • Escape data in admin dashboards too
  • Add a real CSP, not a checkbox CSP
  • Audit dangerous DOM sinks in custom Tito integration code

For broader defensive guidance, the official MDN XSS documentation is worth keeping handy.

The pattern behind most Tito registration XSS bugs is boring: convenience over discipline. A couple of “just this once” innerHTML shortcuts, an admin page nobody thought about, and suddenly your event workflow is executing attendee-supplied JavaScript. That’s fixable, but only if you treat registration data like the untrusted input it is.