Skip to content
Language:en Author:Laborci Gergely
web-security

XSS: when data becomes code

Objectives

By the end of the lesson, you can explain the essence of XSS, you can tell the stored, reflected, and DOM-based variants apart, and you know the layers of protection: output encoding, text and HTML APIs, sanitization, Content Security Policy.

Why is a comment dangerous?

In Kurzuspont students can attach a comment to a course. If the application inserts the text into the page as HTML without checking, the browser interprets the part given by the attacker as code. According to MDN, XSS lets the attacker run code in the vulnerable page's own origin, where the browser treats it as if the author of the page had written it: it can read the page and the local storage, and can request from the server with the user's credentials. The cause of the flaw is always that the boundary between text and executable content is lost.

Three variants

Variant Where does the malicious content get into the page? Example
Stored The server stores it and later displays it to others too. A comment that every visitor sees.
Reflected Data taken from the request (for example a URL parameter) lands in the response. A search page that prints the search word.
DOM-based Client-side code writes data into the DOM in a dangerous way. innerHTML filled from location.search.
flowchart LR
    A[Text given by the attacker] --> S[Application]
    S -->|dangerous HTML insertion| B[Victim's browser]
    B -->|code runs in the vulnerable origin| R[Logged-in action]

Tried: without and with output encoding

The text of the comment is a message from an attacker. A simple server-side template (a JavaScript template string) builds the HTML in two ways:

const komment = '<img src=x onerror="alert(document.cookie)">';
const veszelyes   = c => `<p class="komment">${c}</p>`;
const esc = s => s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')
                  .replace(/"/g,'&quot;').replace(/'/g,'&#39;');
const biztonsagos = c => `<p class="komment">${esc(c)}</p>`;
veszélyes   : <p class="komment"><img src=x onerror="alert(document.cookie)"></p>
biztonságos : <p class="komment">&lt;img src=x onerror=&quot;alert(document.cookie)&quot;&gt;</p>

(In the code and output, komment is the comment, veszelyes/"veszélyes" means "dangerous", and biztonsagos/"biztonságos" means "safe".)

In the first output there is an img element whose error handler (onerror) runs code: the browser interprets this as part of the page. In the second output the characters <, >, and " became HTML entities, so the browser displays it as text, not as an element.

Output encoding is context-dependent: different rules are needed in HTML text, in an attribute, or in JavaScript. In an attribute, even a quotation mark can break out of the value:

without escape: <input value="" onmouseover="alert(1)">
with escape   : <input value="&quot; onmouseover=&quot;alert(1)">

MDN therefore advises using a template engine that performs output encoding, keeping the context in mind (in attributes, always put the value between quotation marks), and regarding script and style contexts as almost always dangerous. The esc function above is a teaching example: in a real application a proven template engine or library must be used, not a home-made solution.

The layers of protection

  1. Output encoding: the template engine should encode by default, and we should know the context.

  2. Text APIs instead of HTML APIs: if only text is needed, we should not interpret it as HTML. In the browser innerHTML interprets the text as HTML, while textContent inserts it as text:

    welcome.innerHTML   = `Üdv, ${nev}!`;   // interpreted as HTML: dangerous
    welcome.textContent = `Üdv, ${nev}!`;   // as text: safe
    

    (The text "Üdv" means "Welcome", and nev is "name".)

  3. Sanitization: if HTML really has to be accepted (a formatted comment), a known, reputable sanitizing library must be used (MDN mentions DOMPurify), not a home-made filter.

  4. Content Security Policy (CSP): a rule set sent to the browser that restricts where the page may load script from. According to MDN its primary purpose is protection against XSS: it can prevent a foreign script from running even if the sanitization failed.

    Content-Security-Policy: script-src 'nonce-{RANDOM}'; object-src 'none'; base-uri 'none'
    

    The nonce is a new random value per response, matching in the header and in the permitted <script nonce="..."> elements; an injected foreign script does not know it. CSP also disables inline event handlers (onclick=...), javascript: addresses, and eval()-like execution. CSP does not replace correct input handling, it is an underlying layer of defense.

  5. Trusted Types: according to MDN, with the CSP directive require-trusted-types-for 'script' it can be enforced that only a value that has passed a policy can reach dangerous places (for example innerHTML). This solution is not a sanitizer but a framework for enforcing sanitization, and its browser support is limited.

What can the attacker do if they get in anyway?

The attacker's code runs in the origin of the vulnerable page, so it can request from the server in the logged-in user's name. A HttpOnly cookie (7.2) cannot be read directly by the script, but it can still start sensitive operations on behalf of the logged-in browser, and can reach the content of localStorage (7.3). XSS can moreover bypass most CSRF protections (8.4), because the request starts from the own origin. HttpOnly thus mitigates, but does not replace preventing XSS.

Worked example: a comment in Kurzuspont

Step Decision
The server stores the text of the comment As raw text, unchanged (we do not store it "cleaned", but encode at display time).
At display time The template engine encodes (HTML context); the client uses textContent.
A formatted comment is needed A sanitizing library with a list of permitted elements.
Fallback CSP with a nonce, forbidding foreign scripts.
Cookie HttpOnly; Secure; SameSite.

An attacker's comment thus appears as text, not as an element, and if an error still slips in, CSP prevents the script from running.

Common misconceptions

Statement Clarification
"HttpOnly prevents all XSS damage." It restricts reading the cookie; code running on the compromised page is capable of other operations.
"It is enough to filter out the <script> text." There are many other routes (event-handler attributes, javascript: addresses); context-dependent encoding is needed.
"HTTPS protects against XSS." It protects the transport, not the application's flaw (4.3).
"CSP is enough if there is one." It is an underlying protection; it does not replace correct output handling.
"The input must be cleaned at storage time." Encoding depends on the context of the output; it must be applied at display time.

In brief

  • With XSS, data becomes code: the attacker's script runs in the origin of the vulnerable page.
  • Variants: stored, reflected, DOM-based.
  • Protection: context-dependent output encoding, textContent, a sanitizing library for HTML, CSP as a fallback.
  • HttpOnly mitigates, but does not protect against the damage of XSS.

Review questions

  1. What is the difference between stored and reflected XSS?
  2. What is the difference between innerHTML and textContent?
  3. Why does CSP not replace output encoding?
  4. Why is a token stored in localStorage dangerous in case of XSS?

Learning resources

Prerequisites (2)

  • HTML
  • DOM

Teaches (3)

  • XSS
  • output encoding
  • Content Security Policy