XSS Vulnerabilities in Frontend System Design: A Practical Guide for Frontend Engineers


Picture this: You've just shipped a beautiful React dashboard for your SaaS product. Users are loving the new comment system, the rich text editor is working perfectly, and your PM is happy. Then, one morning, you get a Slack message that makes your coffee go cold: "We've detected malicious scripts running on user profiles. All hands on deck."
Welcome to the world of Cross-Site Scripting (XSS) vulnerabilities—one of the most common and dangerous security issues in frontend development.
Why XSS Should Keep You Up at Night
XSS isn't just a theoretical security concern that happens to "other people's apps." It's a real threat that can:
Steal user session tokens and authentication cookies
Redirect users to malicious websites
Modify the DOM to create fake login forms
Access sensitive user data and send it to attackers
Turn your application into a launching pad for attacks on other users
The scary part? Most XSS vulnerabilities in modern frontend applications don't come from obvious places like eval()
or innerHTML
. They hide in seemingly innocent features like user profiles, comment systems, notification displays, and rich content editors.
As a frontend engineer, you're the first line of defense. Understanding XSS isn't just about passing interviews—it's about building applications that users can actually trust.
The Three Faces of XSS
Let's break down the three main types of XSS vulnerabilities you'll encounter in real-world frontend applications.
1. Stored XSS: The Persistent Threat
Stored XSS is like a time bomb. The malicious script gets saved to your database and executes every time someone views the infected content.
Here's how it typically happens in a React application:
// UserProfile.js - VULNERABLE CODE
import { useState, useEffect } from 'react';
function UserProfile({ userId }) {
const [profile, setProfile] = useState(null);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => setProfile(data));
}, [userId]);
if (!profile) return <div>Loading...</div>;
return (
<div className="profile">
<h2>{profile.name}</h2>
<div className="bio">
{/* DANGER: This will execute any script tags in the bio */}
<div dangerouslySetInnerHTML={{ __html: profile.bio }} />
</div>
</div>
);
}
How the attack works:
Attacker creates a profile with bio:
<script>fetch('/api/steal-data', {method: 'POST', body: document.cookie})</script>
This gets stored in your database
Every time someone views this profile, the script executes
The attacker now has access to every visitor's session cookies
The fix:
// UserProfile.js - SECURE VERSION
import { useState, useEffect } from 'react';
import DOMPurify from 'dompurify';
function UserProfile({ userId }) {
const [profile, setProfile] = useState(null);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => setProfile(data));
}, [userId]);
if (!profile) return <div>Loading...</div>;
// Sanitize HTML before rendering
const sanitizedBio = DOMPurify.sanitize(profile.bio, {
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'ul', 'ol', 'li'],
ALLOWED_ATTR: []
});
return (
<div className="profile">
<h2>{profile.name}</h2>
<div className="bio">
<div dangerouslySetInnerHTML={{ __html: sanitizedBio }} />
</div>
</div>
);
}
2. Reflected XSS: The Mirror Attack
Reflected XSS happens when user input is immediately reflected back in the response without proper sanitization. This is common in search results, error messages, and URL parameters.
Here's a vulnerable Next.js search page:
// pages/search.js - VULNERABLE CODE
import { useRouter } from 'next/router';
export default function SearchPage() {
const router = useRouter();
const { q } = router.query;
return (
<div>
<h1>Search Results</h1>
{q && (
<p>
{/* DANGER: URL parameter directly rendered */}
You searched for: <span dangerouslySetInnerHTML={{ __html: q }} />
</p>
)}
{/* Search results would go here */}
</div>
);
}
How the attack works:
Attacker crafts a malicious URL:
https://yourapp.com/search?q=<script>alert('XSS')</script>
They trick users into clicking this link (via email, social media, etc.)
When users visit the link, the script executes immediately
The fix:
// pages/search.js - SECURE VERSION
import { useRouter } from 'next/router';
export default function SearchPage() {
const router = useRouter();
const { q } = router.query;
return (
<div>
<h1>Search Results</h1>
{q && (
<p>
{/* React automatically escapes this content */}
You searched for: <span>{q}</span>
</p>
)}
{/* Search results would go here */}
</div>
);
}
The key insight here: React's default behavior is to escape content, which protects you. The vulnerability only appears when you deliberately bypass this protection with dangerouslySetInnerHTML
.
3. DOM-based XSS: The Client-Side Trap
DOM-based XSS happens entirely in the browser, without the server being involved. The malicious payload is processed by client-side JavaScript.
Here's a common pattern in React apps that handle URL fragments:
// components/WelcomeMessage.js - VULNERABLE CODE
import { useEffect, useState } from 'react';
function WelcomeMessage() {
const [message, setMessage] = useState('');
useEffect(() => {
// Getting user name from URL fragment
const hash = window.location.hash;
if (hash.startsWith('#welcome=')) {
const userName = hash.substring(9); // Remove '#welcome='
// DANGER: Directly inserting URL content into DOM
document.getElementById('welcome').innerHTML = `Welcome back, ${userName}!`;
}
}, []);
return <div id="welcome"></div>;
}
How the attack works:
Attacker creates a malicious URL:
https://yourapp.com/#welcome=<img
src=x onerror=alert('XSS')>
The fragment is processed by client-side JavaScript
The malicious code executes without any server interaction
The fix:
// components/WelcomeMessage.js - SECURE VERSION
import { useEffect, useState } from 'react';
function WelcomeMessage() {
const [userName, setUserName] = useState('');
useEffect(() => {
const hash = window.location.hash;
if (hash.startsWith('#welcome=')) {
const name = hash.substring(9);
// Let React handle the escaping
setUserName(decodeURIComponent(name));
}
}, []);
return (
<div>
{userName && <p>Welcome back, {userName}!</p>}
</div>
);
}
XSS in Unexpected Places
Now that you understand the basics, let's explore where XSS vulnerabilities love to hide in modern SaaS applications.
Content Management Systems
Many SaaS apps have marketing pages managed through a CMS. Here's a vulnerable pattern:
// components/MarketingBanner.js - VULNERABLE CODE
import { useState, useEffect } from 'react';
function MarketingBanner() {
const [bannerContent, setBannerContent] = useState('');
useEffect(() => {
fetch('/api/cms/banner')
.then(res => res.json())
.then(data => setBannerContent(data.html));
}, []);
return (
<div
className="marketing-banner"
dangerouslySetInnerHTML={{ __html: bannerContent }}
/>
);
}
The problem: If your CMS doesn't properly sanitize content, or if an attacker gains access to your CMS, they can inject malicious scripts that will run on every page.
Markdown Rendering
Rich text editors and markdown processors are common XSS entry points:
// components/BlogPost.js - VULNERABLE CODE
import { marked } from 'marked';
function BlogPost({ content }) {
const htmlContent = marked(content);
return (
<article
dangerouslySetInnerHTML={{ __html: htmlContent }}
/>
);
}
The secure approach:
// components/BlogPost.js - SECURE VERSION
import { marked } from 'marked';
import DOMPurify from 'dompurify';
function BlogPost({ content }) {
const htmlContent = marked(content);
const sanitizedContent = DOMPurify.sanitize(htmlContent, {
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'h1', 'h2', 'h3', 'ul', 'ol', 'li', 'a', 'code', 'pre'],
ALLOWED_ATTR: ['href', 'title'],
ALLOWED_SCHEMES: ['http', 'https', 'mailto']
});
return (
<article
dangerouslySetInnerHTML={{ __html: sanitizedContent }}
/>
);
}
Notifications and Toasts
Notification systems often display user-generated content, making them prime XSS targets:
// components/NotificationToast.js - VULNERABLE CODE
function NotificationToast({ notification }) {
return (
<div className="toast">
<div
className="toast-message"
dangerouslySetInnerHTML={{ __html: notification.message }}
/>
</div>
);
}
The secure version:
// components/NotificationToast.js - SECURE VERSION
function NotificationToast({ notification }) {
return (
<div className="toast">
<div className="toast-message">
{notification.message}
</div>
</div>
);
}
Safely Rendering User-Generated Content
Let's dive deep into how to handle user-generated content (UGC) securely in React and Next.js applications.
Setting Up DOMPurify
First, install and configure DOMPurify:
npm install dompurify
npm install @types/dompurify # If using TypeScript
Create a utility function for consistent sanitization:
// utils/sanitize.js
import DOMPurify from 'dompurify';
export const sanitizeHtml = (dirty, options = {}) => {
const defaultOptions = {
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'ul', 'ol', 'li', 'a'],
ALLOWED_ATTR: ['href', 'title', 'target'],
ALLOWED_SCHEMES: ['http', 'https', 'mailto'],
FORBID_TAGS: ['script', 'object', 'embed', 'base', 'link'],
FORBID_ATTR: ['style', 'onerror', 'onload', 'onclick']
};
const mergedOptions = { ...defaultOptions, ...options };
return DOMPurify.sanitize(dirty, mergedOptions);
};
// For different content types
export const sanitizeMarkdown = (dirty) => {
return sanitizeHtml(dirty, {
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
'ul', 'ol', 'li', 'a', 'code', 'pre', 'blockquote'],
ALLOWED_ATTR: ['href', 'title']
});
};
export const sanitizeBasicText = (dirty) => {
return sanitizeHtml(dirty, {
ALLOWED_TAGS: ['p', 'br', 'strong', 'em'],
ALLOWED_ATTR: []
});
};
Creating a Safe HTML Component
Build a reusable component for rendering sanitized HTML:
// components/SafeHtml.js
import { sanitizeHtml } from '../utils/sanitize';
function SafeHtml({ html, className = '', sanitizeOptions = {} }) {
const cleanHtml = sanitizeHtml(html, sanitizeOptions);
return (
<div
className={className}
dangerouslySetInnerHTML={{ __html: cleanHtml }}
/>
);
}
export default SafeHtml;
Usage Examples
// components/UserComment.js
import SafeHtml from './SafeHtml';
function UserComment({ comment }) {
return (
<div className="comment">
<div className="comment-author">{comment.author}</div>
<SafeHtml
html={comment.content}
className="comment-content"
sanitizeOptions={{
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'a'],
ALLOWED_ATTR: ['href']
}}
/>
</div>
);
}
Security Best Practices for Frontend Teams
1. Avoid dangerouslySetInnerHTML
When Possible
The best defense against XSS is to avoid rendering raw HTML altogether. React's default escaping behavior protects you:
// GOOD: React automatically escapes this
function UserName({ name }) {
return <span>{name}</span>; // Safe even if name contains <script>
}
// RISKY: Only use when absolutely necessary
function RichContent({ html }) {
return <div dangerouslySetInnerHTML={{ __html: sanitizeHtml(html) }} />;
}
2. Implement Content Security Policy (CSP)
Configure CSP headers in your Next.js application:
// next.config.js
const nextConfig = {
async headers() {
return [
{
source: '/(.*)',
headers: [
{
key: 'Content-Security-Policy',
value: [
"default-src 'self'",
"script-src 'self' 'unsafe-inline'", // Be more restrictive in production
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: https:",
"font-src 'self'",
"connect-src 'self'",
"frame-ancestors 'none'"
].join('; ')
}
]
}
];
}
};
module.exports = nextConfig;
For production, create a more restrictive CSP:
// production CSP
const productionCSP = [
"default-src 'self'",
"script-src 'self'", // Remove 'unsafe-inline'
"style-src 'self'",
"img-src 'self' data: https:",
"font-src 'self'",
"connect-src 'self' https://api.yourservice.com",
"frame-ancestors 'none'",
"base-uri 'self'",
"form-action 'self'"
].join('; ');
3. Input Validation Strategies
Validate user input both on the client and server side:
// utils/validation.js
export const validateUserInput = (input, type = 'text') => {
const validators = {
text: (str) => typeof str === 'string' && str.length <= 1000,
email: (str) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(str),
url: (str) => {
try {
const url = new URL(str);
return ['http:', 'https:'].includes(url.protocol);
} catch {
return false;
}
}
};
return validators[type] ? validators[type](input) : false;
};
// In your component
function CommentForm({ onSubmit }) {
const [comment, setComment] = useState('');
const [error, setError] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
if (!validateUserInput(comment, 'text')) {
setError('Invalid comment content');
return;
}
onSubmit(comment);
};
return (
<form onSubmit={handleSubmit}>
<textarea
value={comment}
onChange={(e) => setComment(e.target.value)}
maxLength={1000}
/>
{error && <div className="error">{error}</div>}
<button type="submit">Post Comment</button>
</form>
);
}
4. Safe URL Handling
Be careful when handling URLs from user input:
// utils/url.js
export const sanitizeUrl = (url) => {
try {
const parsed = new URL(url);
// Only allow http and https protocols
if (!['http:', 'https:'].includes(parsed.protocol)) {
return null;
}
return parsed.toString();
} catch {
return null;
}
};
// In your component
function LinkComponent({ href, children }) {
const safeUrl = sanitizeUrl(href);
if (!safeUrl) {
return <span>{children}</span>;
}
return (
<a
href={safeUrl}
target="_blank"
rel="noopener noreferrer"
>
{children}
</a>
);
}
Interview Insights: How Interviewers Test XSS Knowledge
Understanding XSS isn't just about writing secure code—it's also about demonstrating your security awareness in interviews. Here's how interviewers typically approach this topic and what they're looking for.
Common Interview Questions
Question 1: "Walk me through how you would implement a comment system for a blog application. What security considerations would you keep in mind?"
What they're testing: Your ability to identify XSS risks in a common feature and propose mitigation strategies.
Good answer structure:
Identify the XSS risk: "User comments are a classic XSS vector because they contain user-generated content"
Explain the attack scenario: "An attacker could submit a comment containing script tags that would execute when other users view the comment"
Propose specific solutions: "I would sanitize all user input using a library like DOMPurify, validate input lengths, and implement CSP headers"
Mention additional considerations: "I'd also implement rate limiting, content moderation, and ensure the backend validates and sanitizes data as well"
Question 2: "You're reviewing a React component that uses dangerouslySetInnerHTML
to render user profiles. What red flags would you look for?"
What they're testing: Your code review skills and understanding of XSS vulnerabilities.
Good answer points:
Check if the HTML content is being sanitized before rendering
Look for direct insertion of user input without validation
Verify if the content source is trusted
Consider if
dangerouslySetInnerHTML
is necessary or if plain text rendering would sufficeExamine the allowed HTML tags and attributes
Question 3: "How would you implement a rich text editor that allows users to format their content but prevents XSS attacks?"
What they're testing: Your ability to balance functionality with security.
Good answer structure:
Explain the allowlist approach: "I would define a strict allowlist of safe HTML tags and attributes"
Mention sanitization: "Use DOMPurify to sanitize content both on the client and server"
Discuss CSP: "Implement Content Security Policy to prevent inline scripts"
Consider alternatives: "For simpler use cases, consider markdown instead of raw HTML"
Question 4: "A user reports that clicking a link in your application executed unwanted JavaScript. How would you investigate and fix this?"
What they're testing: Your incident response approach and debugging skills.
Good answer structure:
Immediate response: "First, I'd determine the scope—is this affecting multiple users or just one?"
Investigation: "I'd check for reflected XSS in URL parameters, examine how we handle user-generated links, and review recent code changes"
Mitigation: "Implement proper URL validation, sanitize link targets, and add rel='noopener noreferrer' to external links"
Prevention: "Set up monitoring for suspicious JavaScript execution and implement stricter CSP"
What Interviewers Want to Hear
Security-first mindset: Show that you consider security implications from the beginning of feature development, not as an afterthought.
Practical knowledge: Demonstrate familiarity with real-world tools and libraries (DOMPurify, CSP, etc.) rather than just theoretical concepts.
Defense in depth: Explain how you'd implement multiple layers of protection rather than relying on a single solution.
User impact awareness: Show that you understand how XSS attacks affect real users and business outcomes.
Red Flags to Avoid
Dismissing XSS as "just a frontend problem" (it requires full-stack thinking)
Suggesting overly complex solutions when simple ones exist
Not mentioning user input validation
Ignoring the importance of server-side validation
Focusing only on technical solutions without considering user experience
XSS Prevention Practices Checklist
Here's your comprehensive checklist for preventing XSS vulnerabilities in frontend development:
Development Phase
[ ] Default to safe rendering: Use React's default text rendering instead of
dangerouslySetInnerHTML
whenever possible[ ] Sanitize all user input: Implement DOMPurify or similar sanitization library for any HTML content
[ ] Validate URLs: Check user-provided URLs for safe protocols (http/https only)
[ ] Use allowlists: Define strict allowlists for HTML tags and attributes rather than blocklists
[ ] Implement CSP: Configure Content Security Policy headers to prevent inline script execution
[ ] Validate on both ends: Implement validation on both client and server sides
Code Review Phase
[ ] Audit
dangerouslySetInnerHTML
usage: Every instance should be reviewed and justified[ ] Check user input handling: Verify all user inputs are properly validated and sanitized
[ ] Review third-party content: Ensure external content (APIs, CMSs) is treated as untrusted
[ ] Examine URL handling: Check for proper validation of user-provided URLs and links
[ ] Verify escape sequences: Ensure special characters are properly escaped in all contexts
Testing Phase
[ ] Test with malicious payloads: Try common XSS payloads in all input fields
[ ] Verify CSP effectiveness: Test that your CSP headers actually prevent script injection
[ ] Check different browsers: Ensure XSS protection works across different browsers
[ ] Test edge cases: Verify handling of empty inputs, very long inputs, and special characters
[ ] Validate sanitization: Ensure your sanitization logic handles all expected HTML elements
Deployment Phase
[ ] Configure production CSP: Set strict CSP headers for production environment
[ ] Enable security headers: Implement additional security headers (X-Content-Type-Options, X-Frame-Options)
[ ] Monitor for violations: Set up CSP violation reporting to detect potential attacks
[ ] Regular security audits: Schedule periodic security reviews of user-facing features
[ ] Keep dependencies updated: Regularly update sanitization libraries and security-related packages
Ongoing Maintenance
[ ] Security training: Keep the team updated on latest XSS attack vectors and prevention techniques
[ ] Incident response plan: Have a clear process for handling security incidents
[ ] Documentation: Maintain clear guidelines for secure coding practices
[ ] Penetration testing: Conduct regular security testing of your applications
Remember, security isn't a one-time implementation—it's an ongoing practice that requires constant vigilance and updates. The techniques and tools discussed in this guide will help you build more secure applications, but staying informed about new attack vectors and defense strategies is equally important.
The goal isn't to become a security expert overnight, but to develop a security-conscious mindset that naturally considers these risks as you design and build frontend applications. With practice and attention to these principles, you'll be able to create applications that users can trust with their data and their safety.
Subscribe to my newsletter
Read articles from Kshitij Sharma directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Kshitij Sharma
Kshitij Sharma
A Software Engineer who writes about what he learns in his free time