SurveyMonkey feels harmless because it’s “just forms.” That mindset gets teams into trouble.

I’ve seen developers lock down their main app, then casually embed survey content, pipe responses into dashboards, send answers into admin panels, and render “custom thank you” pages with basically no output encoding. That’s how XSS sneaks in: not through the survey vendor itself, but through the glue code around it.

If you use SurveyMonkey in a website, a customer portal, or an internal reporting tool, the risky parts are usually:

  • embedded survey widgets
  • custom parameters passed into survey pages
  • response viewers and analytics dashboards
  • webhook consumers
  • email templates and confirmation pages
  • admin tools where staff review answers

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

Mistake #1: Trusting survey responses because they came from SurveyMonkey

A survey response is still user input. Every free-text field is attacker-controlled.

If you ingest responses through exports, APIs, webhooks, or copy-paste from an admin console, and then render them into HTML, you have an XSS problem waiting to happen.

Bad:

app.get('/responses/:id', async (req, res) => {
  const response = await db.getResponse(req.params.id);

  res.send(`
    <h1>Survey Response</h1>
    <div>Name: ${response.name}</div>
    <div>Comment: ${response.comment}</div>
  `);
});

If response.comment contains <img src=x onerror=alert(1)>, you just executed it.

Better:

const escapeHtml = (str = '') =>
  str
    .replaceAll('&', '&amp;')
    .replaceAll('<', '&lt;')
    .replaceAll('>', '&gt;')
    .replaceAll('"', '&quot;')
    .replaceAll("'", '&#39;');

app.get('/responses/:id', async (req, res) => {
  const response = await db.getResponse(req.params.id);

  res.send(`
    <h1>Survey Response</h1>
    <div>Name: ${escapeHtml(response.name)}</div>
    <div>Comment: ${escapeHtml(response.comment)}</div>
  `);
});

Even better: use a real templating engine with auto-escaping enabled, and don’t bypass it unless you absolutely have to.

If your frontend is React, Vue, Angular, or Svelte, stick to normal text rendering. Most modern frameworks escape by default. XSS usually appears when someone decides to “support rich text” and starts injecting HTML.

Mistake #2: Using innerHTML for survey answers

This one is still everywhere. Teams fetch survey responses from an API and render them like this:

results.innerHTML = response.comment;

That’s fine only if response.comment is fully trusted HTML, which survey answers are not.

Use textContent for plain text:

results.textContent = response.comment;

If you truly need formatted HTML, sanitize it first with a library built for that job, like DOMPurify.

import DOMPurify from 'dompurify';

results.innerHTML = DOMPurify.sanitize(response.comment);

My rule is simple: if content originated from a survey text box, treat it as hostile forever. Don’t “trust it later” because it passed through your backend.

Mistake #3: Passing URL parameters into custom SurveyMonkey flows without validation

A lot of SurveyMonkey setups involve custom landing pages or redirects:

  • pre-survey intro page
  • post-submission thank-you page
  • campaign tracking wrapper
  • custom embedded experience in your app

Then someone grabs query params and drops them into the page.

Bad:

const params = new URLSearchParams(location.search);
document.getElementById('campaign').innerHTML = params.get('campaign');

That turns a tracking parameter into script execution.

Fix it by validating and encoding:

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

if (/^[a-zA-Z0-9_-]{1,50}$/.test(campaign)) {
  document.getElementById('campaign').textContent = campaign;
} else {
  document.getElementById('campaign').textContent = 'unknown';
}

Whitelist formats whenever possible. Campaign IDs, respondent IDs, language codes, and source tags usually have predictable structure. Validate them as data, not as markup.

Mistake #4: Building custom thank-you pages with unsafe personalization

I see this a lot in marketing stacks: a SurveyMonkey completion redirect points to your site, and your page says:

Thanks, Sarah, for completing the survey.

That name often comes from a query string, local storage, or a backend lookup keyed by an untrusted token.

Bad:

thankYou.innerHTML = `Thanks, ${userName}, for completing the survey.`;

Fix:

thankYou.textContent = `Thanks, ${userName}, for completing the survey.`;

If you need some formatting, separate the text node from the markup:

const strong = document.createElement('strong');
strong.textContent = userName;

thankYou.textContent = 'Thanks, ';
thankYou.appendChild(strong);
thankYou.appendChild(document.createTextNode(' for completing the survey.'));

This is slightly more annoying than innerHTML. That’s the point. Safe code is often a little less convenient.

Mistake #5: Forgetting that internal dashboards are XSS targets too

A nasty pattern: “Only employees can see survey responses, so rendering raw HTML is fine.”

Nope. Internal admin XSS is often worse than public-site XSS because admins have more privileges, more cookies, and access to more systems.

If a malicious respondent can store script in a survey answer, and your support dashboard renders it unsafely, you’ve got stored XSS against staff.

Typical vulnerable code:

function ResponseRow({ answer }) {
  return <div dangerouslySetInnerHTML={{ __html: answer.comment }} />;
}

Unless you sanitize first, this is a bug.

Safer:

function ResponseRow({ answer }) {
  return <div>{answer.comment}</div>;
}

If HTML rendering is unavoidable:

import DOMPurify from 'dompurify';

function ResponseRow({ answer }) {
  const clean = DOMPurify.sanitize(answer.comment);
  return <div dangerouslySetInnerHTML={{ __html: clean }} />;
}

And yes, I’d still prefer plain text unless there’s a hard business reason not to.

Mistake #6: Treating CSP as optional

CSP won’t fix unsafe rendering by itself, but it can turn a bad day into a less catastrophic one.

Survey embeds often push teams toward looser policies because third-party scripts are involved. Then they give up and use unsafe-inline and broad wildcard sources. That defeats most of the value.

A decent CSP for pages around surveys should:

  • restrict script-src to known origins
  • avoid unsafe-inline if possible
  • use nonces or hashes for inline scripts
  • limit frame-src to SurveyMonkey if embedding is required
  • restrict connect-src to the APIs you actually use

Example:

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

You’ll need to adapt this to your exact embed and API usage. If you want the nuts and bolts of nonces, strict CSP, and rollout strategy, csp-guide.com is a solid reference.

Also, test your headers instead of assuming they’re fine. I usually run pages through HeaderTest because it quickly shows whether my CSP and related headers are actually being sent the way I think they are.

Mistake #7: Sanitizing on input and assuming the problem is solved

Teams sometimes sanitize survey responses when they first ingest them, store the “cleaned” version, and call it done.

I don’t love that approach as your only defense.

Why? Because context matters. The encoding needed for HTML text is different from HTML attributes, JavaScript strings, CSS, and URLs. A one-time cleanup step does not make arbitrary content safe everywhere.

What I prefer:

  • validate input when possible
  • store raw data if business/legal requirements allow
  • encode on output for the specific rendering context
  • sanitize only when you intentionally allow limited HTML

That means:

  • HTML body: escape HTML entities
  • attribute values: attribute-encode
  • URLs: validate allowed schemes and encode
  • JavaScript contexts: don’t inject untrusted data directly

This is where a lot of “we already sanitized that” bugs come from.

Sometimes survey answers or metadata become links in reports or admin tools. Maybe respondents can paste a website, portfolio URL, or support reference.

If you render that into an anchor without validating the scheme, you can create script execution or phishing paths.

Bad:

link.href = response.website;
link.textContent = response.website;

Fix:

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

const url = safeUrl(response.website);
if (url) {
  link.href = url;
  link.textContent = url;
} else {
  link.removeAttribute('href');
  link.textContent = 'Invalid URL';
}

If links open in a new tab, add rel="noopener noreferrer" too.

Mistake #9: Ignoring webhook and integration payloads

SurveyMonkey integrations often feed CRMs, Slack bots, ticket systems, and internal apps. The dangerous assumption is that because the payload came from a legitimate webhook endpoint, the fields inside it are trustworthy.

They’re not. The transport can be authentic while the content is still attacker-supplied.

If a survey answer gets pushed into:

  • a support ticket comment
  • an internal notification UI
  • a CRM note
  • a chatbot summary panel

you still need output encoding where it’s displayed.

The source system being trusted does not make the embedded user content trusted.

A practical baseline

If I were reviewing a SurveyMonkey integration for XSS risk, I’d want these boxes checked:

  • no raw survey answers rendered with innerHTML
  • templates use auto-escaping
  • any rich text goes through DOMPurify or equivalent
  • query params and redirect values are validated against allowlists
  • URLs are scheme-validated before becoming links
  • admin dashboards treat responses as hostile input
  • CSP is present and reasonably strict
  • security headers are tested in production, not just locally

Survey tools don’t create some exotic new XSS category. They just increase the number of places where untrusted text flows through your stack. That’s enough.

If you remember one thing, make it this: every survey response is attacker-controlled input, even when it arrives through a polished SaaS platform. Treat it that way from the first webhook to the last dashboard widget.