XSS Prevention
No Script Carnival Today
Web risk is rarely mysterious. It usually lies in predictable mistakes that persist under time pressure.
For XSS Prevention the gains come from context-bound output, browser restrictions and secure frontend baselines.
This makes security less of a separate afterthought and more of a standard quality of your product.
Immediate measures (15 minutes)
Why this matters
The core of XSS Prevention is risk reduction in practice. Technical context supports the choice of measures, but implementation and assurance are central.
Defense: how to do it right
The beauty of the web security industry is that we have had solutions for XSS for twenty years, and that we collectively refuse to use them. We know how it should be done. We have the tools. We have the standards. We have the documentation. And yet, year after year, XSS appears in the OWASP Top 10.
It is as if all of humanity has a manual for preventing fires, but we collectively keep tossing our cigarettes into the dry grass.
But well, for those who actually want to do their job, here are the defenses.
Output Encoding -- the first line of defense
The most fundamental defense against XSS is output encoding: rendering special characters harmless before they end up in the HTML.
< wordt <
> wordt >
" wordt "
' wordt '
& wordt &
Context-specific encoding is essential. HTML encoding protects in the HTML context but not in JavaScript strings. JavaScript encoding protects in JavaScript but not in HTML attributes. URL encoding protects in URLs but not in HTML.
# Python/Flask -- safe (Jinja2 auto-escapes)
{{ user_input }}
# UNSAFE -- | safe disables auto-escaping
{{ user_input | safe }}// JavaScript -- UNSAFE
element.innerHTML = userInput;
// JavaScript -- safe
element.textContent = userInput;The rule is simple: use textContent instead of
innerHTML when rendering text. Use the auto-escaping of
your template engine. And if you ever type | safe
or {!! !!} or dangerouslySetInnerHTML,
stop and ask yourself why.
Content Security Policy (CSP) -- the second line of defense
Content Security Policy is an HTTP header that tells the browser which sources a page is allowed to load. It is the most powerful defense against XSS, and at the same time the most underutilized.
Content-Security-Policy: default-src 'self';
script-src 'self';
style-src 'self' 'unsafe-inline';
img-src 'self' data:;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
With this header the page can only load scripts from its own
domain ('self'). Inline scripts
(<script>alert(1)</script>) are blocked.
Event handler attributes (onerror=alert(1)) are
blocked. eval() is blocked.
Important CSP directives for XSS prevention:
| Directive | Function |
|---|---|
script-src |
Which sources may serve scripts |
style-src |
Which sources may serve stylesheets |
default-src |
Fallback for all resource types |
frame-ancestors |
Who may load this page in an iframe |
base-uri |
Which base URIs are allowed |
form-action |
Where forms may submit to |
Nonce-based CSP:
Content-Security-Policy: script-src 'nonce-R4nd0mStr1ng';
<!-- This script will execute (correct nonce) -->
<script nonce="R4nd0mStr1ng">
// Legitimate application code
</script>
<!-- This script will be blocked (no nonce) -->
<script>alert('XSS')</script>With nonce-based CSP all scripts must have a unique, per-request generated nonce. An attacker who injects XSS does not know the nonce and therefore cannot execute scripts.
Strict-dynamic:
Content-Security-Policy: script-src 'strict-dynamic' 'nonce-R4nd0m';
With strict-dynamic, scripts loaded by a trusted
script (with the correct nonce) are also allowed to load scripts
themselves. This makes it easier to implement CSP in complex
applications with many dynamically loaded scripts.
HttpOnly Cookies
Set-Cookie: session=abc123; HttpOnly; Secure; SameSite=Strict
The HttpOnly flag prevents JavaScript from reading
the cookie via document.cookie. This specifically protects
against the most direct form of XSS exploitation: cookie theft.
But -- and this is important -- HttpOnly does not
protect against:
- CSRF via XSS (the browser sends cookies automatically)
- Keylogging
- DOM manipulation
- Phishing via page replacement
- localStorage theft
HttpOnly is a lock on one of the doors. Better than
nothing, but not a complete solution.
DOMPurify -- sanitization for the client side
// UNSAFE
element.innerHTML = userInput;
// SAFE with DOMPurify
element.innerHTML = DOMPurify.sanitize(userInput);DOMPurify is a JavaScript library that sanitizes HTML by removing all potentially dangerous elements and attributes. It is the industry standard for client-side HTML sanitization.
// DOMPurify configuration examples
DOMPurify.sanitize(dirty, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a'],
ALLOWED_ATTR: ['href']
});Specific defenses per XSS type
| XSS Type | Primary defense | Secondary defense |
|---|---|---|
| Reflected | Output encoding | CSP, input validation |
| Stored | Output encoding | CSP, input sanitization |
| DOM-based | Use textContent |
DOMPurify, CSP |
For DOM-based XSS specifically:
- Avoid
innerHTML,document.write(),eval() - Use
textContentorsetAttribute()instead ofinnerHTML - Validate the
originforpostMessageevents:
window.addEventListener('message', function(event) {
// ALWAYS check the origin!
if (event.origin !== 'https://vertrouwd-domein.nl') {
return; // Ignore messages from unknown sources
}
// Use textContent, NOT innerHTML
document.getElementById('notificatie').textContent = event.data;
});The cynical truth about "it's just JavaScript"
There is a certain type of manager in the tech industry -- you know them, those people with their polo shirts and their "agile" buzzwords -- who, at every pentest report containing an XSS finding, say: "But it's just a pop-up. How bad can it be?"
It's just a pop-up. Exactly. And a nuclear reactor is just a water boiler. And a gun is just a tube with a spring. The oversimplification misses the point so spectacularly that it is almost admirable.
It's just JavaScript. That same JavaScript that:
- Can take over your session
- Can log your passwords
- Can replace your screen with a phishing page
- Can execute transactions on your behalf
- Can exfiltrate your corporate data
- Can install a persistent backdoor in your browser
"It's just JavaScript" is the security equivalent of "it's just a little water" while the dam is breaking.
The problem is not that managers do not understand what JavaScript can do. The problem is that they do not want to understand what JavaScript can do. Because if they did, they would also have to understand that their application has been vulnerable for years, that they have been making the wrong choices for years, and that fixing it costs money. And it is always easier to wave away the risk than to address it.
That is why we make those nice alert(1) pop-ups. Not
because that is the only thing we can do, but because it is the simplest
way to prove that code execution is possible. The real exploit comes
after. And you demonstrate it in a controlled environment, with the
right tooling, so that even the most cynical manager can no longer deny
the risk.
Prototype Pollution defense
For prototype pollution, specific defensive measures apply:
1. Use Object.create(null) for
dictionaries:
// UNSAFE -- inherits from Object.prototype
let config = {};
// SAFE -- no prototype
let config = Object.create(null);2. Use Map instead of plain
objects:
// UNSAFE
let cache = {};
cache[userInput] = value;
// SAFE
let cache = new Map();
cache.set(userInput, value);3. Check with hasOwnProperty:
function safeMerge(target, source) {
for (let key in source) {
if (source.hasOwnProperty(key)) {
if (key === '__proto__' || key === 'constructor') {
continue; // Skip dangerous keys
}
target[key] = source[key];
}
}
return target;
}4. Update your dependencies:
lodash>= 4.6.2- Avoid
deep-extend,hoistjs, and other known vulnerable libraries
5. Freeze the prototype:
This is the nuclear option: nobody can add properties to
Object.prototype anymore. It may break legitimate code
that extends prototypes, but it completely eliminates prototype
pollution.
Node.js Injection defense
1. Never use eval() on user
input:
// NEVER DO THIS
app.get('/calc', (req, res) => {
let result = eval(req.query.expr); // RCE!
res.send(String(result));
});
// SAFE alternative -- use a parser
const mathjs = require('mathjs');
app.get('/calc', (req, res) => {
let result = mathjs.evaluate(req.query.expr);
res.send(String(result));
});2. JSON.parse() is safe, eval()
on JSON is not:
3. Use the --frozen-intrinsics
flag:
This freezes all built-in objects, making prototype pollution impossible.
4. Avoid vm as a sandbox:
The vm module of Node.js is not a security sandbox.
Use vm2 or isolated-vm if you truly need to
execute code in a sandbox, but be aware that these libraries can also
have vulnerabilities.
Summary: the XSS toolbox
In this chapter we have journeyed from the simplest
<script>alert(1) </script> to a fully
operational XSS command and control system. Let us line up the most
important lessons.
XSS Types
| Type | Payload location | Server involved | Requires click |
|---|---|---|---|
| Reflected | URL parameters | Yes | Yes |
| Stored | Database/file | Yes | No |
| DOM-based | URL fragment/DOM | No | Sometimes |
Defense overview
| Measure | Protects against | Effectiveness |
|---|---|---|
| Output encoding | Reflected, Stored XSS | High |
| CSP (nonce-based) | All XSS types | Very high |
| HttpOnly cookies | Cookie theft via XSS | Medium |
| DOMPurify | DOM-based XSS | High |
| textContent | DOM-based XSS | High |
| Object.create(null) | Prototype pollution | High |
| JSON.parse | Node.js injection (vs eval) | High |
Further reading and references
| Source | Topic |
|---|---|
| OWASP XSS Prevention Cheat Sheet | Output encoding per context |
| OWASP Testing Guide - XSS | Testing methodology |
| PortSwigger Web Security Academy - XSS | Interactive labs |
| Google CSP Evaluator | CSP policy testing |
| DOMPurify GitHub repository | Client-side sanitization |
| CWE-79: Improper Neutralization of Input | Formal vulnerability definition |
| CWE-1321: Improperly Controlled Modification | Prototype pollution CWE |
| NodeGoat OWASP Project | Node.js security labs |
| HackTricks - XSS | Extensive payload collection |
| Browser security test suite | Browser security validation |
The next time someone tells you that XSS is "just a pop-up," invite them to a demonstration in a controlled lab environment. Nothing convinces as quickly as seeing your own keystrokes on someone else's screen.
In the next chapter we dive into Server-Side Request Forgery (SSRF) -- a vulnerability where we use the server itself as a proxy to reach places we should not be. If XSS is about manipulating the browser, SSRF is about manipulating the server. And servers typically have access to far more interesting things than browsers.
Further reading in the knowledge base
These articles in the portal give you more background and practical context:
- APIs -- the invisible glue of the internet
- SSL/TLS -- why that padlock in your browser matters
- Encryption -- the art of making things unreadable
- Password hashing -- how websites store your password
- Penetration tests vs. vulnerability scans
You need an account to access the knowledge base. Log in or register.
Related security measures
These articles provide additional context and depth: