Safeguard
Developer Tools•

innerHTML vs textContent: avoiding DOM XSS in plain JavaScript

Setting innerHTML with untrusted data is a classic DOM XSS sink. Use textContent for text, sanitize if you need markup, and know which sinks to audit.

Safeguard Security Team
3 min read

Use textContent whenever you are inserting text, because it treats the value as text and never as markup. Use innerHTML only with HTML you have sanitized with a maintained library. Passing untrusted strings straight to innerHTML is one of the most common causes of DOM based XSS.

The difference in one example

// Risky: the string is parsed as HTML
el.innerHTML = userComment;

// Safe for text: the string is displayed as is
el.textContent = userComment;

If userComment contains markup, the first line builds elements from it. The second line shows the characters on screen.

OWASP's guidance is to refactor code to remove references to unsafe sinks like innerHTML, and to use textContent or value instead. One detail from the same page: textContent does not encode anything, so pass the original text. Encoding first would show entities to the user.

Sinks to look for

MDN groups injection sinks into three types in its Trusted Types documentation:

  • HTML sinks that interpret input as HTML, such as innerHTML and document.write().
  • JavaScript sinks that run input as code, such as eval() and the text property of a script element.
  • Script URL sinks, such as the src property of a script element.

Safe alternatives that treat data as text include textContent, insertAdjacentText(), value and setAttribute() for plain attributes. Note that setAttribute is not safe for event handler attributes or URL attributes like href unless you validate the value.

When you need markup

Sometimes users must write formatted text. Then sanitize before you assign it:

el.innerHTML = DOMPurify.sanitize(untrustedHtml);

OWASP recommends DOMPurify for this and adds three rules. Sanitize as late as possible, because changing the string afterwards can undo the protection. Check that nothing downstream mutates the sanitized value. Keep the library patched.

Building elements without parsing

For structured output, build nodes in code.

const li = document.createElement("li");
li.textContent = item.name;
list.appendChild(li);

This is easy to review. There is no string of markup to get wrong.

Add a net underneath

Even careful teams miss a sink. Two browser features catch mistakes:

  • A Content Security Policy without unsafe-inline blocks inline script and event handler attributes.
  • Trusted Types makes the browser throw an error when a plain string reaches an HTML or script sink. MDN lists the API as Baseline 2026, newly available. Our Trusted Types explainer covers the rollout.

A five minute audit

  1. Search for innerHTML, outerHTML, insertAdjacentHTML, document.write and eval.
  2. For each hit, trace where the value comes from.
  3. Replace with textContent or node creation where markup is not needed.
  4. Wrap the remainder in a sanitizer.
  5. Re-run the search in code review so new hits are caught.

Quick check

Is innerHTML safe if I strip the script tags first?

No. Markup can run script without a script tag, for example through event handler attributes. Remove the sink or use a maintained sanitizer.

Sources

Last reviewed: 19 October 2026.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.