Decloak is an AI-powered web security intelligence platform that scans a site's HTTP/TLS posture, JavaScript, and third-party scripts to produce a report anyone can read. This guide is part of Decloak's library of practical, source-backed security guidance.
In this guide
- Key takeaways
- What are the most dangerous ways to inject user data into the DOM?
- How do I encode output for the right context?
- When must I render user - provided HTML, how can I do it safely?
- How does Trusted Types add a safety net for dangerous sinks?
- What should a strong Content - Security - Policy look like?
- Which JavaScript APIs should I avoid completely?
- How do modern frameworks help prevent XSS?
- Why is server - side validation still important?
- How do I keep my XSS defenses up to date?
- Quick checklist for JavaScript code
- Comparison of safe vs unsafe DOM insertion methods
Key takeaways
- Use only text - based DOM APIs (
textContent,setAttribute,appendChild) for untrusted data. - Encode output for the exact context (HTML, attribute, URL, JS) before insertion.
- Run any required HTML through a modern sanitizer like DOMPurify.
- Enable Trusted Types and a strict Content - Security - Policy as a second line of defense.
- Never use dangerous JavaScript APIs (
eval,new Function,document.write) with user - controlled strings.
What are the most dangerous ways to inject user data into the DOM?
Directly inserting untrusted strings into innerHTML, outerHTML, insertAdjacentHTML or document.write executes any embedded script. Use text - only APIs such as textContent, innerText, setAttribute (with quoted values), or DOM construction methods (createElement, appendChild). These APIs treat the value as plain text, so markup characters are not parsed as code.
// Bad - executes any <script> in userInput
element.innerHTML = userInput;
// Good - renders userInput as harmless text
element.textContent = userInput;
How do I encode output for the right context?
Encoding must match where the data ends up. For HTML body use <, >, &, ", '. For attributes also quote the value. For JavaScript strings escape backslashes and quotes. Use a templating engine that performs context - aware encoding automatically (Handlebars, EJS, React JSX, etc.).
function encodeHTML(str) {
return str.replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":"'"}[c]));
}
const safe = encodeHTML(userInput);
element.innerHTML = safe; // now safe because characters are encoded
When must I render user - provided HTML, how can I do it safely?
If the feature requires rich - text (comments, editor output), sanitize the string first with a well - maintained library such as DOMPurify.
import DOMPurify from 'dompurify';
const cleanHtml = DOMPurify.sanitize(userHtml);
contentDiv.innerHTML = cleanHtml;
The sanitizer removes <script> tags, event - handler attributes and javascript: URLs, preventing execution.
How does Trusted Types add a safety net for dangerous sinks?
Trusted Types let you define a policy that only returns a TrustedHTML object after sanitization. Assigning a raw string to innerHTML without passing through the policy throws a TypeError. Enable the CSP directive require - trusted - types - for 'script' to enforce it.
const policy = trustedTypes.createPolicy('default', {
createHTML: input => DOMPurify.sanitize(input)
});
// This works
element.innerHTML = policy.createHTML(userHtml);
// This throws
element.innerHTML = userHtml; // TypeError
What should a strong Content - Security - Policy look like?
A CSP header blocks execution of any script that is not explicitly allowed. Use nonces or hashes for the few inline scripts you must keep, block unsafe-inline and unsafe-eval, and restrict script sources to trusted origins.
Content-Security-Policy: script-src 'nonce-<random>' https://cdn.example.com;
object-src 'none';
base-uri 'self';
require-trusted-types-for 'script';
If malicious markup slips through, the browser will refuse to run the injected code.
Which JavaScript APIs should I avoid completely?
Never pass user - controlled strings to:
eval()new Function()setTimeout(string)/setInterval(string)document.write()These APIs evaluate the string as code, giving any attacker immediate execution rights.
How do modern frameworks help prevent XSS?
Frameworks such as React, Angular, and Vue automatically escape data in templates. Only explicit "dangerous" APIs like dangerouslySetInnerHTML (React) or v-html (Vue) bypass this protection. Keep those features disabled unless you have a strong reason and have already sanitized the content.
Why is server - side validation still important?
Client - side checks can be bypassed. Validate and whitelist input on the server (e.g., allow only alphanumeric usernames, enforce length limits, reject HTML tags) so that malicious payloads never reach the browser in the first place.
How do I keep my XSS defenses up to date?
- Regularly update libraries such as DOMPurify and your templating engine.
- Track security advisories for the browsers you support; newer versions enforce CSP and Trusted Types more strictly.
- Run Decloak’s free scan on your site to get a quick report of any XSS - related findings across its eight core layers.
Quick checklist for JavaScript code
- Sanitize any HTML you must render →
DOMPurify.sanitize(userHtml). - Encode all other data →
element.textContent = userInput;or use a safe templating engine. - Never use
innerHTML,outerHTML,insertAdjacentHTML,document.write,eval,setTimeout(string), etc., with untrusted data. - Enable Trusted Types (
trustedTypes.createPolicy) and addrequire-trusted-types-for 'script'to CSP. - Set a strict CSP header with nonces/hashes and no
unsafe-inlineorunsafe-eval.
Comparison of safe vs unsafe DOM insertion methods
| Method | Handles untrusted data safely? | Typical use case |
|---|---|---|
textContent / innerText | Yes - treats value as plain text | Displaying user comments, usernames |
setAttribute (quoted) | Yes - attribute value is escaped | Adding src or href attributes |
createElement + appendChild | Yes - builds nodes without parsing HTML | Constructing dynamic UI components |
innerHTML | No - parses HTML and executes scripts | Legacy code that injects markup |
insertAdjacentHTML | No - same risks as innerHTML | Quick HTML insertion shortcuts |
document.write | No - writes raw markup into the document stream | Rarely needed in modern apps |
By following these concrete steps, you can eliminate the most common XSS vectors in JavaScript applications and rely on layered defenses that protect even if one layer is missed.
Related guides
Is a Content - Security - Policy (CSP) header necessary?
A CSP header is not required by the HTTP spec, but it is a widely - recommended defense - in - depth control that mitigates XSS and click - jacking. Learn why you should deploy it and how to do it correctly.
How can I view the Content-Security-Policy header in Chrome?
Learn the exact steps to open Chrome DevTools, locate the Network tab, and read the CSP header from the response headers of the main document.
What are common cross - site scripting (XSS) attack examples and how to detect them?
Learn concrete XSS examples - reflected, stored, DOM - based, attribute - based, encoded, cookie - grabber and error - page attacks - and see how Decloak’s free scan can surface them quickly.