Paperform makes it easy to ship forms fast. That convenience is also where teams get sloppy. I’ve seen the same pattern over and over: a safe hosted form gets wrapped in unsafe frontend code, piped through insecure automations, or embedded into pages that trust user input way too much.

Paperform itself isn’t the whole story. XSS usually shows up in the code around it: confirmation pages, embed wrappers, analytics hooks, custom HTML blocks, webhook consumers, and admin dashboards displaying submissions.

Here are the most common mistakes I see with XSS in Paperform setups, and how to fix them.

Mistake 1: Trusting form responses when rendering them later

A Paperform submission is user input. That sounds obvious, but teams forget it the second they display that data in an internal dashboard, CRM sync preview, email template builder, or a “thank you” page.

Bad code usually looks like this:

const submission = await fetch("/api/submission/123").then(r => r.json());

document.getElementById("preview").innerHTML = `
  <h3>${submission.name}</h3>
  <p>${submission.message}</p>
`;

If submission.message contains <img src=x onerror=alert(1)>, you’ve just executed attacker-controlled JavaScript.

Fix

Use textContent for plain text. Treat everything from Paperform as untrusted unless you have explicitly sanitized it for a very specific HTML policy.

const submission = await fetch("/api/submission/123").then(r => r.json());

document.getElementById("name").textContent = submission.name;
document.getElementById("message").textContent = submission.message;

If you really need rich text, sanitize it on the server or with a well-reviewed sanitizer before rendering. Don’t build your own regex-based HTML cleaner. That path ends badly.

Mistake 2: Using innerHTML in custom thank-you flows

A common Paperform pattern is redirecting users to a custom page and passing values in query parameters. Then somebody reads those params and drops them straight into the DOM.

Example:

const params = new URLSearchParams(window.location.search);
const name = params.get("name");

document.getElementById("thanks").innerHTML = `Thanks, ${name}!`;

That turns your thank-you page into an XSS sink.

Fix

Never inject query params with innerHTML. Use textContent.

const params = new URLSearchParams(window.location.search);
const name = params.get("name") || "friend";

document.getElementById("thanks").textContent = `Thanks, ${name}!`;

If you need to support formatting, define it yourself in code. Don’t let the user provide HTML.

Mistake 3: Assuming embed code makes the whole page safe

Paperform embeds are usually dropped into existing marketing sites, React apps, Shopify themes, or CMS pages. The form may be isolated enough, but the surrounding page often is not.

I’ve seen teams do this right next to the embed:

<div id="paperform-wrapper"></div>
<div id="debug-output"></div>

<script>
  const qp = new URLSearchParams(location.search);
  document.getElementById("debug-output").innerHTML = qp.get("ref");
</script>

The problem isn’t the Paperform embed. It’s the rest of the page.

Fix

Audit the entire page, not just the form snippet. Search for dangerous DOM sinks:

  • innerHTML
  • outerHTML
  • insertAdjacentHTML
  • document.write
  • eval
  • new Function
  • string-based setTimeout and setInterval

Safer version:

const qp = new URLSearchParams(location.search);
document.getElementById("debug-output").textContent = qp.get("ref") || "";

If your page hosts third-party scripts, put guardrails around them with a Content Security Policy. For practical CSP rollout details, https://csp-guide.com is useful. For the browser-side rules themselves, use official docs like MDN’s CSP reference.

Mistake 4: Rendering submission data inside HTML attributes

This one is sneaky. Developers avoid innerHTML in visible content, then inject user data into attributes:

button.outerHTML = `<button data-email="${submission.email}">Contact user</button>`;

If the attacker breaks out of the attribute, they can inject event handlers or malformed markup depending on context.

Fix

Create elements with the DOM API and set attributes safely.

const button = document.createElement("button");
button.textContent = "Contact user";
button.dataset.email = submission.email;
container.replaceChildren(button);

Context matters. HTML text, HTML attributes, JavaScript strings, URL values, and CSS all have different escaping rules. The easiest way to win is to avoid raw HTML construction entirely.

Mistake 5: Passing Paperform data into inline scripts

I still see pages doing this in server-rendered templates:

<script>
  window.submission = {
    name: "{{ submission.name }}",
    comment: "{{ submission.comment }}"
  };
</script>

If your templating doesn’t correctly escape for JavaScript string context, an attacker can break out of the string and run code.

Fix

Serialize data as JSON with the template engine’s safe JSON helper, not string interpolation.

Example in principle:

<script type="application/json" id="submission-data">
  {{ submission | json }}
</script>
<script>
  const submission = JSON.parse(
    document.getElementById("submission-data").textContent
  );
</script>

That pattern is far less fragile than hand-building JavaScript objects in templates.

Mistake 6: Allowing arbitrary HTML in custom fields or admin notes

Sometimes teams intentionally allow “basic formatting” in follow-up notes, internal comments, or custom confirmation content tied to Paperform workflows. Then they forget to sanitize consistently across every place it gets rendered.

The bug usually isn’t where the content is entered. It’s where it gets displayed later in some forgotten admin page.

Fix

Pick one rule and stick to it:

  • If the field is plain text, store and render it as plain text.
  • If the field allows HTML, sanitize it at input or output with a strict allowlist.

Be opinionated about what HTML is allowed. Most teams don’t need <iframe>, inline event handlers, SVG, or custom data URLs. “Rich text” should usually mean a tiny subset like <b>, <i>, <p>, <ul>, and <a> with safe URL validation.

Mistake 7: Ignoring webhook consumers

Paperform often sends submissions to your backend through webhooks or automation tools. The webhook payload itself won’t trigger XSS on the server, but the second you log it into an admin panel or replay it into a frontend app, you’re back in the danger zone.

I’ve seen this exact flow:

  1. User submits payload with script tags
  2. Backend stores it safely
  3. Admin dashboard renders it with innerHTML
  4. Admin account gets popped

Fix

Threat-model the full lifecycle of a submission:

  • ingestion
  • storage
  • moderation
  • export
  • display
  • email preview
  • admin search results

Every display point must encode output for its context. Stored XSS is usually more damaging than reflected XSS because it hits privileged users.

Mistake 8: Weak or missing CSP on pages with forms

CSP won’t fix unsafe DOM code, but it does reduce blast radius. If someone finds an XSS sink near your Paperform integration, a decent CSP can stop inline script execution or restrict data exfiltration.

Too many sites either have no CSP or a fake one with unsafe-inline and unsafe-eval everywhere.

Fix

Start with a policy that blocks inline scripts unless you explicitly nonce or hash them.

A basic example:

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-r4nd0m';
  object-src 'none';
  base-uri 'self';
  frame-ancestors 'self';

Real Paperform embed requirements depend on how you load it, so test carefully and allow only the minimum required sources. Roll it out in report-only mode first if needed. Paperform-specific integration details should come from Paperform documentation, and CSP behavior from official browser docs like MDN.

Mistake 9: Sanitizing on input only

Teams sometimes sanitize when the submission first arrives, then assume the data is permanently safe. That breaks down fast when requirements change. Maybe today the field is displayed in HTML text, tomorrow it’s inserted into a URL parameter, and next month it lands in a CSV export.

Fix

Validate on input, encode on output.

That split matters:

  • Validation rejects malformed or unexpected data early
  • Sanitization is for carefully allowed HTML
  • Output encoding happens at render time for the exact context

If a field is supposed to be an email, validate it as an email. If it’s supposed to be a name, reject control characters and absurd lengths. Then still render it safely later.

Mistake 10: Forgetting framework escape hatches

React, Vue, Svelte, Next.js, Rails, Laravel, Django — most modern stacks escape output by default. XSS bugs usually appear when developers bypass the safe defaults.

A few examples:

  • React dangerouslySetInnerHTML
  • Vue v-html
  • raw Blade output
  • Django safe
  • template filters that disable escaping

I’ve lost count of how many “Paperform XSS” reports were really “someone used the framework’s unsafe HTML escape hatch with submission data.”

Fix

Stay on the framework’s escaped path unless there’s a hard requirement for HTML rendering. If you must render HTML, sanitize first and isolate that logic in one place.

React example:

export function SubmissionCard({ submission }) {
  return (
    <div>
      <h3>{submission.name}</h3>
      <p>{submission.message}</p>
    </div>
  );
}

Not this:

export function SubmissionCard({ submission }) {
  return <div dangerouslySetInnerHTML={{ __html: submission.message }} />;
}

A practical checklist for Paperform XSS prevention

When I review a Paperform integration, I check these first:

  • Are any submission values rendered with innerHTML?
  • Are query params or hash fragments shown on thank-you pages?
  • Are webhook payloads displayed in an admin UI?
  • Is custom HTML allowed anywhere in the flow?
  • Are framework escape hatches being used?
  • Is there a CSP that actually restricts script execution?
  • Are redirects, previews, and exports treated as untrusted data flows?

If you fix only one thing, fix the rendering paths around Paperform. That’s where most of the real XSS bugs live. The form is just the entry point. Your app is where the payload usually lands.