2026-08-12

What is XSS and How to Prevent It? Complete Web Guide

Learn how Cross-Site Scripting (XSS) attacks work and how to prevent them using Context-Aware Output Encoding, Content Security Policy (CSP), and sanitization.

securityweb-developmenthtmlcybersecurity
  • Cross-Site Scripting (XSS) allows attackers to inject malicious client-side scripts into web pages viewed by other users.
  • XSS vulnerabilities fall into three primary categories: Stored (Persistent), Reflected, and DOM-Based attacks.
  • Context-aware output encoding and safe DOM manipulation APIs serve as the primary defensive controls against code injection.
  • Content Security Policy (CSP) headers and HttpOnly cookie flags provide crucial defense-in-depth layers.

Cross-Site Scripting (XSS) represents one of the most pervasive threats to client-side web application security. The vulnerability occurs when an application includes untrusted, user-supplied data in an HTML document without proper encoding or sanitization. The victim's web browser cannot determine whether the script should be trusted and executes it within the user's security context.

An attacker executing arbitrary JavaScript can exfiltrate sensitive session cookies, hijack user accounts, log keystrokes, capture clipboard data, or force the browser into executing unintended state-changing actions.

Core Categories of Cross-Site Scripting

XSS flaws are classified according to how the payload is stored and processed:

| Vulnerability Type | Payload Origin | Execution Trigger | Severity Impact | |---|---|---|---| | Stored (Persistent) | Database, comment feed, user profiles | Any visitor opening the compromised resource | Critical | | Reflected (Non-Persistent) | HTTP request parameters, search queries | Clicking a crafted malicious URL or form | High | | DOM-Based | Client-side sources (location.hash, referrer) | Client-side JavaScript executing unsafe DOM sinks | High |

Stored XSS poses the highest risk because the injected payload persists directly in the application's backend storage. When an attacker posts a comment containing a payload, the database returns that markup to every subsequent user viewing the post. The attack targets thousands of users automatically without requiring tailored phishing links.

Reflected XSS occurs when an application immediately returns user input without database persistence. A search form that renders user search strings directly from the URL query parameter is vulnerable. The attacker must distribute the malicious link to victims directly.

DOM-Based XSS executes entirely on the client side without server-side markup modifications. Client scripts read input from an untrusted source (such as window.location.search) and write it to an execution sink (such as element.innerHTML or document.write).

Dangerous DOM Manipulation Patterns

Directly assigning unsanitized strings to HTML-parsing sinks creates instant DOM XSS vulnerabilities:

// Vulnerable client-side DOM manipulation
const queryParams = new URLSearchParams(window.location.search);
const userHandle = queryParams.get('handle');

// Insecure: innerHTML parses HTML tags and runs scripts
document.getElementById('profile-heading').innerHTML = 'Profile: ' + userHandle;

If an attacker crafts a link containing a query parameter with an onerror handler calling fetch('/exfil?c='+document.cookie), the browser triggers the handler and executes the exfiltration script immediately.

Replacing innerHTML with textContent or innerText eliminates parsing vulnerabilities because browsers treat the input strictly as literal character data:

// Secure client-side DOM manipulation
const headingElement = document.getElementById('profile-heading');
headingElement.textContent = 'Profile: ' + userHandle;

To encode raw input characters safely before rendering them in dynamic templates, use an HTML entity encoder to map special characters to safe entities, and maintain clean document hierarchies with our HTML formatter.

Context-Aware Output Encoding Rules

Applying the correct encoding format depends on the specific destination context within the HTML document structure:

  • HTML Body Context: Convert structural control characters to corresponding HTML entities (< becomes &lt;, > becomes &gt;, & becomes &amp;, " becomes &quot;, ' becomes &#27;).
  • HTML Attribute Context: Ensure attributes are strictly quoted with double quotes and encode both quotes and ampersands. Unquoted attributes permit attackers to break out using whitespace delimiters.
  • JavaScript Variable Context: Avoid placing raw user variables directly inside inline script tags. Serialize objects with JSON serializers and escape forward slashes and angle brackets (\u003C) to prevent premature script tag closures.
  • URL Context: Validate protocols against an allowlist (http:, https:). Reject dangerous URI schemes such as javascript: or data:.

While backend data access controls prevent SQL injection as discussed in our SQL injection guide, client-side safety requires strict context-aware encoding rules.

Content Security Policy (CSP) Implementation

Content Security Policy (CSP) provides a robust browser-level defense-in-depth mechanism. Sent via the Content-Security-Policy HTTP response header, CSP restricts which script sources, styles, and external connections the browser is permitted to load.

Standard hardened CSP header configuration:

Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-k7R3n8Xp92'; object-src 'none'; base-uri 'self';

Key security directives in this policy:

  • Suppression of unsafe-inline: Inline script tags and inline event handlers (onclick=, onerror=) are blocked automatically.
  • Cryptographic Nonces: The server generates a unique, unguessable base64 string per request. Only script tags containing matching nonce attributes execute.
  • object-src 'none': Disables legacy plugin vectors including Flash and Java applets.

For sensitive session tokens and authentication cookies, enforce the HttpOnly cookie attribute. Browsers restrict document.cookie access for HttpOnly cookies, preventing automated script payloads from accessing the token directly during an XSS event.

Modern frontend libraries such as React, Vue, and Angular perform automatic HTML encoding for data bindings ({userName}) by default. However, bypass methods like React's dangerouslySetInnerHTML or Vue's v-html disable this protection. When rendering rich text formatting from user submissions is mandatory, pass inputs through a battle-tested sanitization library like DOMPurify before inserting markup into the DOM.

Frequently Asked Questions

Do modern JavaScript frameworks eliminate all XSS vulnerabilities?

No. While React, Vue, and Angular automatically escape variables rendered inside standard JSX or template interpolations, vulnerabilities re-emerge when developers use raw HTML escape hatches (dangerouslySetInnerHTML, v-html), bind user input to dangerous URL schemes, or manipulate the native DOM directly with innerHTML.

No. HttpOnly prevents malicious JavaScript from reading the cookie value directly through document.cookie. However, an injected script running in the victim's session can still issue authenticated requests (via fetch or XMLHttpRequest), extract sensitive DOM data, or construct phishing overlays to capture passwords.

Is stripping script tags with regular expressions safe?

No. Attackers easily bypass regex-based tag strippers using alternative vector tags, event attributes (onerror=, onload=), inline svg elements, frame wrappers, case variations, or nested payloads. Reliable prevention requires strict character encoding or robust sanitization parsers.

Can a Content Security Policy replace output encoding?

No. CSP is an additional layer of defense designed to mitigate exploitation when an injection flaw exists. A misconfigured policy (such as one allowing 'unsafe-inline' or broad wildcard domains) can allow script execution. Context-aware output encoding remains the primary defense.