React Security JavaScript Frontend XSS CSRF
React Security Vulnerabilities: Common Attacks and Defenses
React Security Vulnerabilities: Common Attacks and Defenses
Introduction
React applications are vulnerable to various security attacks. Understanding these vulnerabilities and implementing proper defenses is crucial for building secure web applications.
1. Cross-Site Scripting (XSS) Attacks
Stored XSS Vulnerability
// ❌ VULNERABLE: Direct innerHTML usage
const UserProfile = ({ user }) => {
return (
<div>
<h1>Welcome, {user.name}</h1>
<div dangerouslySetInnerHTML={{ __html: user.bio }} />
</div>
);
};
// ✅ SECURE: Sanitize and escape content
import DOMPurify from 'dompurify';
const SecureUserProfile = ({ user }) => {
const sanitizedBio = DOMPurify.sanitize(user.bio);
return (
<div>
<h1>Welcome, {user.name}</h1>
<div dangerouslySetInnerHTML={{ __html: sanitizedBio }} />
</div>
);
};
Reflected XSS Prevention
// ❌ VULNERABLE: Direct URL parameter usage
const SearchResults = () => {
const [searchTerm] = useSearchParams();
return (
<div>
<h2>Results for: {searchTerm.get('q')}</h2>
</div>
);
};
// ✅ SECURE: Escape and validate input
import { escape } from 'html-escaper';
const SecureSearchResults = () => {
const [searchTerm] = useSearchParams();
const query = searchTerm.get('q') || '';
const escapedQuery = escape(query);
return (
<div>
<h2>Results for: {escapedQuery}</h2>
</div>
);
};
2. Cross-Site Request Forgery (CSRF) Protection
CSRF Token Implementation
// ✅ SECURE: CSRF token in requests
import { useState, useEffect } from 'react';
const SecureForm = () => {
const [csrfToken, setCsrfToken] = useState('');
const [formData, setFormData] = useState({});
useEffect(() => {
// Fetch CSRF token on component mount
fetch('/api/csrf-token')
.then(res => res.json())
.then(data => setCsrfToken(data.token));
}, []);
const handleSubmit = async (e) => {
e.preventDefault();
const response = await fetch('/api/submit', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken,
},
body: JSON.stringify(formData),
});
if (response.ok) {
console.log('Form submitted successfully');
}
};
return (
<form onSubmit={handleSubmit}>
<input
type="hidden"
name="csrf_token"
value={csrfToken}
/>
<input
type="text"
name="username"
onChange={(e) => setFormData({...formData, username: e.target.value})}
/>
<button type="submit">Submit</button>
</form>
);
};
SameSite Cookie Protection
// ✅ SECURE: Configure cookies with SameSite
const loginUser = async (credentials) => {
const response = await fetch('/api/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include', // Include cookies
body: JSON.stringify(credentials),
});
if (response.ok) {
// Cookie will be set with SameSite=Strict
window.location.href = '/dashboard';
}
};
3. Authentication Vulnerabilities
JWT Token Security
// ❌ VULNERABLE: Storing JWT in localStorage
const login = async (credentials) => {
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(credentials),
});
const { token } = await response.json();
localStorage.setItem('token', token); // ❌ Vulnerable to XSS
};
// ✅ SECURE: Use httpOnly cookies
const secureLogin = async (credentials) => {
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include', // Cookies are automatically handled
body: JSON.stringify(credentials),
});
if (response.ok) {
// Token is stored in httpOnly cookie
window.location.href = '/dashboard';
}
};
Secure Authentication Context
// ✅ SECURE: Authentication context with proper token handling
import { createContext, useContext, useState, useEffect } from 'react';
const AuthContext = createContext();
export const AuthProvider = ({ children }) => {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Verify token on app load
verifyToken();
}, []);
const verifyToken = async () => {
try {
const response = await fetch('/api/verify', {
credentials: 'include',
});
if (response.ok) {
const userData = await response.json();
setUser(userData);
} else {
setUser(null);
}
} catch (error) {
setUser(null);
} finally {
setLoading(false);
}
};
const login = async (credentials) => {
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify(credentials),
});
if (response.ok) {
await verifyToken();
return true;
}
return false;
};
const logout = async () => {
await fetch('/api/logout', {
method: 'POST',
credentials: 'include',
});
setUser(null);
};
return (
<AuthContext.Provider value={{ user, login, logout, loading }}>
{children}
</AuthContext.Provider>
);
};
export const useAuth = () => {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within AuthProvider');
}
return context;
};
4. Input Validation and Sanitization
Form Validation with Security
import { useState } from 'react';
import { escape } from 'html-escaper';
const SecureForm = () => {
const [formData, setFormData] = useState({
name: '',
email: '',
message: ''
});
const [errors, setErrors] = useState({});
const validateInput = (name, value) => {
const sanitizedValue = escape(value.trim());
switch (name) {
case 'name':
if (sanitizedValue.length < 2) {
return 'Name must be at least 2 characters';
}
if (!/^[a-zA-Z\s]+$/.test(sanitizedValue)) {
return 'Name can only contain letters and spaces';
}
break;
case 'email':
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(sanitizedValue)) {
return 'Please enter a valid email address';
}
break;
case 'message':
if (sanitizedValue.length > 1000) {
return 'Message must be less than 1000 characters';
}
break;
}
return '';
};
const handleChange = (e) => {
const { name, value } = e.target;
const error = validateInput(name, value);
setFormData(prev => ({
...prev,
[name]: value
}));
setErrors(prev => ({
...prev,
[name]: error
}));
};
const handleSubmit = async (e) => {
e.preventDefault();
// Final validation
const finalErrors = {};
Object.keys(formData).forEach(key => {
const error = validateInput(key, formData[key]);
if (error) finalErrors[key] = error;
});
if (Object.keys(finalErrors).length > 0) {
setErrors(finalErrors);
return;
}
// Submit sanitized data
const sanitizedData = Object.keys(formData).reduce((acc, key) => {
acc[key] = escape(formData[key].trim());
return acc;
}, {});
try {
const response = await fetch('/api/contact', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(sanitizedData),
});
if (response.ok) {
alert('Form submitted successfully!');
}
} catch (error) {
console.error('Error submitting form:', error);
}
};
return (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="name">Name:</label>
<input
type="text"
id="name"
name="name"
value={formData.name}
onChange={handleChange}
/>
{errors.name && <span className="error">{errors.name}</span>}
</div>
<div>
<label htmlFor="email">Email:</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleChange}
/>
{errors.email && <span className="error">{errors.email}</span>}
</div>
<div>
<label htmlFor="message">Message:</label>
<textarea
id="message"
name="message"
value={formData.message}
onChange={handleChange}
/>
{errors.message && <span className="error">{errors.message}</span>}
</div>
<button type="submit">Submit</button>
</form>
);
};
5. Content Security Policy (CSP)
CSP Implementation
// ✅ SECURE: CSP headers in meta tags
const App = () => {
useEffect(() => {
// Set CSP meta tag
const meta = document.createElement('meta');
meta.httpEquiv = 'Content-Security-Policy';
meta.content = `
default-src 'self';
script-src 'self' 'unsafe-inline' https://trusted-cdn.com;
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
font-src 'self' https://fonts.gstatic.com;
img-src 'self' data: https:;
connect-src 'self' https://api.example.com;
`;
document.head.appendChild(meta);
}, []);
return (
<div>
<h1>Secure React App</h1>
</div>
);
};
Secure Image Loading
// ✅ SECURE: Validate image sources
const SecureImage = ({ src, alt, ...props }) => {
const [isValid, setIsValid] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
const validateImage = async () => {
try {
// Validate URL format
const url = new URL(src);
if (!['https:', 'data:'].includes(url.protocol)) {
throw new Error('Invalid protocol');
}
// Check if image loads successfully
const img = new Image();
img.onload = () => setIsValid(true);
img.onerror = () => setError('Failed to load image');
img.src = src;
} catch (err) {
setError('Invalid image URL');
}
};
validateImage();
}, [src]);
if (error) {
return <div className="image-error">{error}</div>;
}
if (!isValid) {
return <div className="image-loading">Loading...</div>;
}
return <img src={src} alt={alt} {...props} />;
};
6. Secure API Communication
API Request Security
// ✅ SECURE: Centralized API client with security
class SecureApiClient {
constructor() {
this.baseURL = process.env.REACT_APP_API_URL;
this.defaultHeaders = {
'Content-Type': 'application/json',
};
}
async request(endpoint, options = {}) {
const url = `${this.baseURL}${endpoint}`;
const config = {
...options,
headers: {
...this.defaultHeaders,
...options.headers,
},
credentials: 'include', // Include cookies
};
try {
const response = await fetch(url, config);
if (!response.ok) {
if (response.status === 401) {
// Handle unauthorized
window.location.href = '/login';
return;
}
throw new Error(`HTTP ${response.status}`);
}
return await response.json();
} catch (error) {
console.error('API request failed:', error);
throw error;
}
}
async get(endpoint) {
return this.request(endpoint, { method: 'GET' });
}
async post(endpoint, data) {
return this.request(endpoint, {
method: 'POST',
body: JSON.stringify(data),
});
}
}
const apiClient = new SecureApiClient();
7. Security Best Practices Summary
- Sanitize all user inputs - Use DOMPurify and html-escaper
- Implement CSRF protection - Use tokens and SameSite cookies
- Use httpOnly cookies - Store tokens securely
- Validate on client and server - Never trust client-side validation
- Implement CSP headers - Prevent XSS attacks
- Use HTTPS only - Never send sensitive data over HTTP
- Regular security audits - Use tools like OWASP ZAP
- Keep dependencies updated - Regularly update packages
- Implement proper error handling - Don’t expose sensitive information
- Use security headers - Implement proper HTTP security headers
8. Security Testing Tools
# Install security testing tools
npm install --save-dev eslint-plugin-security
npm install --save-dev @typescript-eslint/eslint-plugin
# Add to .eslintrc.js
module.exports = {
plugins: ['security'],
extends: [
'plugin:security/recommended'
],
rules: {
'security/detect-object-injection': 'error',
'security/detect-non-literal-regexp': 'error',
'security/detect-unsafe-regex': 'error'
}
};
Conclusion
React security requires constant vigilance and understanding of attack vectors. Always follow security best practices and use battle-tested libraries for security features.