Carrd is deceptively simple. That’s why people get sloppy with it.

You drag in text, forms, embeds, maybe a custom code block, publish, and move on. From an XSS perspective, that “simple landing page builder” can still become a script execution surface if you treat every HTML box, embed, and third-party widget like a safe sandbox. It isn’t.

This guide is the practical version: where XSS shows up in Carrd, what Carrd does and doesn’t protect you from, and what to actually paste into your setup.

The short version

If you only remember five things:

  1. Never inject untrusted input into custom embeds or custom code.
  2. Treat every third-party script as privileged code.
  3. Escape user-controlled data before rendering it into HTML.
  4. Prefer text rendering over innerHTML.
  5. Set a CSP if you’re on a Carrd setup that allows custom headers via your delivery layer.

If you want to validate what headers your Carrd site is actually returning, HeaderTest is a quick sanity check.

Where XSS happens in Carrd

Carrd itself reduces a lot of risk because most content is configured, not coded. The trouble starts when you add anything dynamic.

The usual XSS entry points are:

  • Embed elements containing custom HTML/JS
  • Third-party forms that reflect submitted values
  • Custom code snippets for analytics, chat widgets, popups, countdowns
  • Query-string-driven personalization
  • Injected HTML from external APIs
  • Unsafe markdown-to-HTML or CMS snippets embedded into Carrd

Carrd is not magically vulnerable by default. The weak point is usually your custom code running inside the page.

Dangerous patterns to avoid

1. Writing query params directly into the DOM

A classic mistake:

<div id="welcome"></div>
<script>
  const params = new URLSearchParams(window.location.search);
  const name = params.get('name');
  document.getElementById('welcome').innerHTML = `Welcome, ${name}`;
</script>

If someone visits:

https://example.carrd.co/?name=<img src=x onerror=alert(1)>

You just executed attacker-controlled HTML.

Safe version

Use textContent instead of innerHTML:

<div id="welcome"></div>
<script>
  const params = new URLSearchParams(window.location.search);
  const name = params.get('name') || 'friend';
  document.getElementById('welcome').textContent = `Welcome, ${name}`;
</script>

That one change kills a huge class of DOM XSS.

If you must render HTML, sanitize it first

Sometimes you really do need HTML. Maybe you’re pulling preformatted content from a trusted CMS or rendering rich snippets.

If you’re doing that in Carrd via an embed, sanitize first with DOMPurify.

<div id="content"></div>
<script src="https://unpkg.com/[email protected]/dist/purify.min.js"></script>
<script>
  const params = new URLSearchParams(window.location.search);
  const raw = params.get('content') || '<p>Hello</p>';

  const clean = DOMPurify.sanitize(raw, {
    ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],
    ALLOWED_ATTR: ['href', 'target', 'rel']
  });

  document.getElementById('content').innerHTML = clean;
</script>

My opinion: if the source is user-controlled, don’t render HTML at all unless you absolutely have to.

Carrd forms often connect to:

  • native Carrd form handling
  • Mailchimp
  • ConvertKit
  • Zapier
  • Formspree
  • custom webhooks

The problem usually isn’t the form submission itself. It’s what happens after submission:

  • reflecting a submitted value in a success message
  • pushing form input into a CRM that later renders unsafe HTML
  • displaying entries in an admin dashboard without escaping

Bad success message logic

<div id="message"></div>
<script>
  const email = new URLSearchParams(location.search).get('email');
  if (email) {
    document.getElementById('message').innerHTML = `Thanks, ${email}!`;
  }
</script>

Safe success message logic

<div id="message"></div>
<script>
  const email = new URLSearchParams(location.search).get('email');
  if (email) {
    document.getElementById('message').textContent = `Thanks, ${email}!`;
  }
</script>

Also: validate on the backend. Frontend validation is for UX, not security.

Third-party widgets are a giant trust decision

People drop scripts into Carrd all the time:

  • live chat
  • popup managers
  • A/B testing
  • social feeds
  • analytics addons
  • booking widgets

Every one of those scripts can read the DOM, modify forms, inject markup, and exfiltrate data. From an XSS standpoint, a compromised third-party script is functionally equivalent to XSS.

If you add this:

<script src="https://random-widget.example/widget.js"></script>

you’ve given that vendor full execution rights on your page.

My rule: if a widget isn’t essential, don’t install it. If it is essential, limit what else runs on the page and review the vendor like you would any dependency.

Avoid inline event handlers

This stuff still shows up in embeds:

<button onclick="doSomething()">Click me</button>

Inline handlers are bad for maintainability and bad for CSP. Prefer this:

<button id="cta">Click me</button>
<script>
  document.getElementById('cta').addEventListener('click', doSomething);
</script>

That makes it easier to deploy a stricter Content Security Policy later.

Escaping utilities you can paste into custom code

If you’re forced to work with dynamic strings and can’t rely entirely on textContent, use a tiny escaping helper.

<script>
  function escapeHTML(str) {
    return String(str)
      .replace(/&/g, '&amp;')
      .replace(/</g, '&lt;')
      .replace(/>/g, '&gt;')
      .replace(/"/g, '&quot;')
      .replace(/'/g, '&#39;');
  }

  const params = new URLSearchParams(location.search);
  const name = params.get('name') || 'guest';

  document.getElementById('output').innerHTML =
    '<p>Hello ' + escapeHTML(name) + '</p>';
</script>

Still, I’d rather use textContent whenever possible.

JSON in script blocks: another common footgun

I’ve seen people embed API data directly into script tags inside Carrd.

Bad

<script>
  const userData = {
    name: "{{ user_name }}"
  };
</script>

If templating ever injects unescaped content, you can break out of the string or script context.

Safer pattern

Render JSON from a trusted backend with proper escaping, or fetch it asynchronously:

<script>
  async function loadProfile() {
    const res = await fetch('https://api.example.com/profile', {
      credentials: 'omit'
    });
    const data = await res.json();
    document.getElementById('name').textContent = data.name;
  }

  loadProfile();
</script>

CSP on Carrd

CSP won’t fix unsafe code, but it does reduce blast radius. If you can control headers through your custom domain setup, reverse proxy, or edge layer, use it.

A reasonable starting policy:

Content-Security-Policy:
  default-src 'self';
  script-src 'self' https://cdn.jsdelivr.net https://unpkg.com;
  style-src 'self' 'unsafe-inline';
  img-src 'self' data: https:;
  font-src 'self' https: data:;
  connect-src 'self' https://api.example.com;
  frame-src https://trusted-widget.example;
  object-src 'none';
  base-uri 'self';
  form-action 'self';

If you rely on inline scripts in Carrd embeds, you may need 'unsafe-inline' in script-src, but that weakens the policy a lot. Better to migrate away from inline JS where possible.

For deeper CSP patterns, reporting, and nonce/hash strategies, csp-guide.com is worth keeping open in another tab.

Safe embed checklist for Carrd

Before publishing a Carrd page with custom code, check these:

  • No innerHTML with untrusted input
  • No inline event handlers
  • No reflection of query params into HTML
  • No raw HTML from forms, webhooks, or APIs
  • Only trusted third-party scripts
  • CSP enabled if your hosting path supports it
  • Headers verified after deploy
  • Dependencies pinned to known versions where possible

Good and bad examples

Bad: query param promo banner

<div id="promo"></div>
<script>
  const promo = new URLSearchParams(location.search).get('promo');
  document.getElementById('promo').innerHTML = promo;
</script>

Good: text-only promo banner

<div id="promo"></div>
<script>
  const promo = new URLSearchParams(location.search).get('promo');
  document.getElementById('promo').textContent = promo || '';
</script>

Better: allow only known promo codes

<div id="promo"></div>
<script>
  const promo = new URLSearchParams(location.search).get('promo');
  const promos = {
    spring: 'Spring discount applied',
    vip: 'VIP access enabled',
    beta: 'Welcome, beta tester'
  };

  document.getElementById('promo').textContent = promos[promo] || '';
</script>

That whitelist pattern is boring, but boring is what you want in security.

Final advice for Carrd developers

Carrd is safest when you keep it mostly static. The minute you start treating it like a mini app platform, you inherit the usual browser security problems.

My default stance is:

  • use Carrd for presentation
  • keep dynamic behavior minimal
  • never trust URL parameters
  • never trust third-party scripts blindly
  • never render user input as HTML unless sanitized

If you follow those rules, most Carrd XSS issues disappear before they start.