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
innerHTMLanddocument.write(). - JavaScript sinks that run input as code, such as
eval()and thetextproperty of a script element. - Script URL sinks, such as the
srcproperty 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-inlineblocks 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
- Search for
innerHTML,outerHTML,insertAdjacentHTML,document.writeandeval. - For each hit, trace where the value comes from.
- Replace with
textContentor node creation where markup is not needed. - Wrap the remainder in a sanitizer.
- 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.