Security

What is Cross-Site Scripting (XSS), and how can it be prevented?

I once inherited a codebase that rendered user comments with innerHTML. No sanitization. A user had already injected a script tag that redirected every vi...

14 Mar 2024

What is Cross-Site Scripting (XSS), and how can it be prevented?

I once inherited a codebase that rendered user comments with innerHTML. No sanitization. A user had already injected a script tag that redirected every visitor to a phishing site. That is XSS. It takes about 30 seconds to exploit and can take weeks to clean up.

XSS lets an attacker run their JavaScript in your users' browsers. The browser trusts it because it comes from your domain. That trust gives the attacker access to cookies, session tokens, DOM content, and anything else your JavaScript can touch.

Three types of XSS

Reflected XSS. The malicious script is part of the request. The server reflects it back in the response. A crafted URL is all the attacker needs. Click the link, the script runs.

Stored XSS. The script is saved to the database. Every user who views that record executes the script. Forum posts, user profiles, comments. Anywhere user content is stored and displayed.

DOM-based XSS. The server is not involved. Client-side JavaScript reads from an untrusted source (like window.location.hash) and writes it into the DOM without sanitization. The attack lives entirely in the browser.

Prevention

Sanitize input. Strip or encode HTML tags from user input before storing it. This is your first line of defense.

Js
function sanitizeInput(input) {
  return input.replace(/<[^>]*>/g, '');
}

Escape output. Even if you trust your stored data, escape it before rendering. Use textContent instead of innerHTML. If you must render HTML, use a proven sanitization library like DOMPurify.

Js
function escapeHTML(str) {
  const div = document.createElement('div');
  div.appendChild(document.createTextNode(str));
  return div.innerHTML;
}

Content Security Policy (CSP). Tell the browser which scripts are allowed to run. A strict CSP blocks inline scripts and restricts script sources to your own domain.

Js
res.setHeader('Content-Security-Policy', "script-src 'self'");

This single header kills most XSS attacks. If an attacker injects a script tag, the browser refuses to execute it because it did not come from an allowed source. The trade-off: you cannot use inline scripts or eval() either. That is a feature, not a bug.

HTTPOnly cookies. Mark session cookies as httpOnly. JavaScript cannot read them. If an XSS attack does execute, it cannot steal the session token.

Js
res.cookie('session', '123456', { httpOnly: true });

Safe DOM manipulation. Use textContent for inserting user data into the page. Never use innerHTML with unsanitized input. In React, JSX escapes by default, but dangerouslySetInnerHTML bypasses that protection.

Js
element.textContent = userContent;

Defense in depth

No single technique stops all XSS. Sanitize input. Escape output. Set CSP headers. Use HTTPOnly cookies. Use safe DOM APIs. Each layer catches what the others miss.

The most common mistake is relying on only one of these. I have seen applications with perfect input sanitization but no CSP, meaning a single bypass in the sanitizer gives the attacker everything. Layer your defenses. Assume each one will fail.

Keep reading