Frontend Security Best Practices
Complete guide to securing your frontend applications against common vulnerabilities
Frontend security is crucial—your users trust you with their data, and attackers are always looking for vulnerabilities. Here's a comprehensive guide to building secure frontend applications that protect both your users and your application.
Common Frontend Security Threats
1. Cross-Site Scripting (XSS)
XSS attacks inject malicious scripts into your pages, which can steal user data, hijack sessions, or deface your site.
Types of XSS
| Type | Description | Example |
|---|---|---|
| Stored XSS | Malicious script stored in database | User comment with <script>alert('XSS')</script> |
| Reflected XSS | Script reflected from URL parameters | ?search=<script>alert('XSS')</script> |
| DOM-based XSS | Script executed through DOM manipulation | location.hash containing malicious code |
Prevention Strategies
// 1. Input Validation and Sanitization
function sanitizeInput(input) {
// Remove or escape dangerous characters
return input
.replace(/[<>]/g, '') // Remove < and >
.replace(/javascript:/gi, '') // Remove javascript: protocol
.replace(/on\w+=/gi, ''); // Remove event handlers
}
// 2. Use DOMPurify for HTML content
import DOMPurify from 'dompurify';
const userInput = '<img src=x onerror=alert("XSS")>';
const cleanHTML = DOMPurify.sanitize(userInput, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a'],
ALLOWED_ATTR: ['href']
});
// 3. Content Security Policy (CSP)
// Add to your HTML head:
// <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline';">
// 4. React's built-in XSS protection
function SafeComponent({ userInput }) {
// React automatically escapes content
return <div>{userInput}</div>; // Safe!
}
// 5. Avoid dangerous APIs
// Don't do this:
// element.innerHTML = userInput;
// Do this instead:
element.textContent = userInput;2. Cross-Site Request Forgery (CSRF)
CSRF attacks trick users into performing unwanted actions on sites where they're authenticated.
Prevention Strategies
// 1. CSRF Tokens
class CSRFProtection {
constructor() {
this.token = this.generateToken();
}
generateToken() {
return crypto.getRandomValues(new Uint8Array(32))
.reduce((acc, val) => acc + val.toString(16).padStart(2, '0'), '');
}
addTokenToRequest(request) {
request.headers['X-CSRF-Token'] = this.token;
return request;
}
validateToken(token) {
return token === this.token;
}
}
// 2. SameSite Cookies
// Set cookies with SameSite attribute
document.cookie = "sessionId=abc123; SameSite=Strict; Secure; HttpOnly";
// 3. Double Submit Cookie Pattern
function submitWithCSRF(formData) {
const csrfToken = getCSRFToken(); // Get from cookie or meta tag
formData.append('csrf_token', csrfToken);
fetch('/api/submit', {
method: 'POST',
body: formData,
credentials: 'include' // Include cookies
});
}3. Clickjacking
Clickjacking tricks users into clicking on invisible or disguised elements.
Prevention Strategies
// 1. Frame Busting
if (window.top !== window.self) {
window.top.location = window.self.location;
}
// 2. X-Frame-Options Header
// Set on server: X-Frame-Options: DENY
// Or: X-Frame-Options: SAMEORIGIN
// 3. Content Security Policy Frame Ancestors
// Add to CSP: frame-ancestors 'none';
// Or: frame-ancestors 'self';
// 4. Modern approach using CSP
const cspHeader = "frame-ancestors 'none';";Authentication & Authorization
Secure Authentication Implementation
// 1. Secure Password Requirements
class PasswordValidator {
static validate(password) {
const requirements = {
minLength: 8,
hasUpperCase: /[A-Z]/.test(password),
hasLowerCase: /[a-z]/.test(password),
hasNumbers: /\d/.test(password),
hasSpecialChar: /[!@#$%^&*(),.?":{}|<>]/.test(password)
};
const errors = [];
if (password.length < requirements.minLength) {
errors.push(`Password must be at least ${requirements.minLength} characters`);
}
if (!requirements.hasUpperCase) errors.push('Include uppercase letter');
if (!requirements.hasLowerCase) errors.push('Include lowercase letter');
if (!requirements.hasNumbers) errors.push('Include number');
if (!requirements.hasSpecialChar) errors.push('Include special character');
return {
isValid: errors.length === 0,
errors
};
}
}
// 2. Secure Token Storage
class SecureTokenManager {
constructor() {
this.tokenKey = 'auth_token';
}
storeToken(token) {
// Store in memory for sensitive apps, or use secure storage
sessionStorage.setItem(this.tokenKey, token);
}
getToken() {
return sessionStorage.getItem(this.tokenKey);
}
removeToken() {
sessionStorage.removeItem(this.tokenKey);
}
// For more security, use httpOnly cookies (server-side)
}
// 3. JWT Token Validation
class JWTValidator {
static validateToken(token) {
try {
const payload = JSON.parse(atob(token.split('.')[1]));
const now = Math.floor(Date.now() / 1000);
if (payload.exp && payload.exp < now) {
return { valid: false, reason: 'Token expired' };
}
return { valid: true, payload };
} catch (error) {
return { valid: false, reason: 'Invalid token' };
}
}
}Authorization Patterns
// 1. Role-Based Access Control (RBAC)
class RBACManager {
constructor() {
this.roles = {
admin: ['read', 'write', 'delete', 'manage_users'],
editor: ['read', 'write'],
viewer: ['read']
};
}
hasPermission(userRole, action) {
return this.roles[userRole]?.includes(action) || false;
}
canAccess(userRole, resource) {
const permissions = {
'admin-panel': ['admin'],
'edit-content': ['admin', 'editor'],
'view-content': ['admin', 'editor', 'viewer']
};
return permissions[resource]?.includes(userRole) || false;
}
}
// 2. Component-Level Authorization
function ProtectedComponent({ user, requiredRole, children }) {
const rbac = new RBACManager();
if (!rbac.canAccess(user.role, requiredRole)) {
return <div>Access Denied</div>;
}
return children;
}
// Usage
<ProtectedComponent user={currentUser} requiredRole="admin-panel">
<AdminPanel />
</ProtectedComponent>Data Protection
Sensitive Data Handling
// 1. Secure Data Transmission
class SecureAPI {
constructor(baseURL) {
this.baseURL = baseURL;
}
async request(endpoint, options = {}) {
const token = new SecureTokenManager().getToken();
const config = {
...options,
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
...options.headers
},
credentials: 'include' // Include cookies
};
// Always use HTTPS in production
const url = `${this.baseURL}${endpoint}`;
try {
const response = await fetch(url, config);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return await response.json();
} catch (error) {
console.error('API request failed:', error);
throw error;
}
}
}
// 2. Data Encryption (Client-side)
class DataEncryption {
static async encrypt(data, key) {
const encoder = new TextEncoder();
const dataBuffer = encoder.encode(JSON.stringify(data));
const cryptoKey = await crypto.subtle.importKey(
'raw',
encoder.encode(key),
{ name: 'AES-GCM' },
false,
['encrypt']
);
const iv = crypto.getRandomValues(new Uint8Array(12));
const encrypted = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv },
cryptoKey,
dataBuffer
);
return {
data: Array.from(new Uint8Array(encrypted)),
iv: Array.from(iv)
};
}
static async decrypt(encryptedData, key, iv) {
const decoder = new TextDecoder();
const encoder = new TextEncoder();
const cryptoKey = await crypto.subtle.importKey(
'raw',
encoder.encode(key),
{ name: 'AES-GCM' },
false,
['decrypt']
);
const decrypted = await crypto.subtle.decrypt(
{ name: 'AES-GCM', iv: new Uint8Array(iv) },
cryptoKey,
new Uint8Array(encryptedData)
);
return JSON.parse(decoder.decode(decrypted));
}
}
// 3. Secure Local Storage
class SecureStorage {
constructor(encryptionKey) {
this.key = encryptionKey;
}
async setItem(key, value) {
const encrypted = await DataEncryption.encrypt(value, this.key);
localStorage.setItem(key, JSON.stringify(encrypted));
}
async getItem(key) {
const encrypted = localStorage.getItem(key);
if (!encrypted) return null;
const { data, iv } = JSON.parse(encrypted);
return await DataEncryption.decrypt(data, this.key, iv);
}
removeItem(key) {
localStorage.removeItem(key);
}
}Security Headers & Configuration
Essential Security Headers
// Express.js middleware for security headers
app.use((req, res, next) => {
// Prevent XSS attacks
res.setHeader('X-XSS-Protection', '1; mode=block');
// Prevent MIME type sniffing
res.setHeader('X-Content-Type-Options', 'nosniff');
// Prevent clickjacking
res.setHeader('X-Frame-Options', 'DENY');
// Strict Transport Security (HTTPS only)
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
// Content Security Policy
res.setHeader('Content-Security-Policy', [
"default-src 'self'",
"script-src 'self' 'unsafe-inline' https://cdn.example.com",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: https:",
"font-src 'self' https://fonts.gstatic.com",
"connect-src 'self' https://api.example.com",
"frame-ancestors 'none'"
].join('; '));
// Referrer Policy
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
// Permissions Policy
res.setHeader('Permissions-Policy', [
'geolocation=()',
'microphone=()',
'camera=()',
'payment=()'
].join(', '));
next();
});Content Security Policy (CSP)
<!-- Comprehensive CSP Example -->
<meta http-equiv="Content-Security-Policy" content="
default-src 'self';
script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net;
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;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
upgrade-insecure-requests;
">Input Validation & Sanitization
Comprehensive Input Validation
// 1. Input Validation Library
class InputValidator {
static patterns = {
email: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
phone: /^\+?[\d\s\-\(\)]{10,}$/,
url: /^https?:\/\/[^\s/$.?#].[^\s]*$/i,
username: /^[a-zA-Z0-9_]{3,20}$/,
password: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/
};
static validate(type, value, options = {}) {
const pattern = this.patterns[type];
if (!pattern) {
throw new Error(`Unknown validation type: ${type}`);
}
const isValid = pattern.test(value);
if (!isValid && options.message) {
throw new Error(options.message);
}
return isValid;
}
static sanitize(input, allowedTags = []) {
// Remove all HTML tags except allowed ones
const tagRegex = /<\/?([a-z][a-z0-9]*)\b[^>]*>/gi;
return input.replace(tagRegex, (match, tag) => {
return allowedTags.includes(tag.toLowerCase()) ? match : '';
});
}
}
// 2. Form Validation Hook
function useFormValidation() {
const [errors, setErrors] = useState({});
const [values, setValues] = useState({});
const validate = (name, value, rules) => {
const fieldErrors = [];
rules.forEach(rule => {
switch (rule.type) {
case 'required':
if (!value || value.trim() === '') {
fieldErrors.push(rule.message || 'This field is required');
}
break;
case 'pattern':
if (!InputValidator.validate(rule.pattern, value)) {
fieldErrors.push(rule.message || 'Invalid format');
}
break;
case 'minLength':
if (value.length < rule.value) {
fieldErrors.push(rule.message || `Minimum ${rule.value} characters`);
}
break;
case 'maxLength':
if (value.length > rule.value) {
fieldErrors.push(rule.message || `Maximum ${rule.value} characters`);
}
break;
}
});
setErrors(prev => ({
...prev,
[name]: fieldErrors
}));
return fieldErrors.length === 0;
};
return { values, errors, validate, setValues };
}
// 3. Usage Example
function SecureForm() {
const { values, errors, validate, setValues } = useFormValidation();
const handleSubmit = (e) => {
e.preventDefault();
const isEmailValid = validate('email', values.email, [
{ type: 'required', message: 'Email is required' },
{ type: 'pattern', pattern: 'email', message: 'Invalid email format' }
]);
const isPasswordValid = validate('password', values.password, [
{ type: 'required', message: 'Password is required' },
{ type: 'pattern', pattern: 'password', message: 'Password too weak' }
]);
if (isEmailValid && isPasswordValid) {
// Submit form
console.log('Form is valid');
}
};
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={values.email || ''}
onChange={(e) => setValues({ ...values, email: e.target.value })}
onBlur={() => validate('email', values.email, [
{ type: 'required' },
{ type: 'pattern', pattern: 'email' }
])}
/>
{errors.email && <div className="error">{errors.email.join(', ')}</div>}
<input
type="password"
value={values.password || ''}
onChange={(e) => setValues({ ...values, password: e.target.value })}
onBlur={() => validate('password', values.password, [
{ type: 'required' },
{ type: 'pattern', pattern: 'password' }
])}
/>
{errors.password && <div className="error">{errors.password.join(', ')}</div>}
<button type="submit">Submit</button>
</form>
);
}Error Handling & Logging
Secure Error Handling
// 1. Error Boundary for React
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
// Log error securely (don't expose sensitive data)
console.error('Application error:', {
message: error.message,
stack: error.stack,
componentStack: errorInfo.componentStack,
timestamp: new Date().toISOString()
});
// Send to error reporting service
this.logError(error, errorInfo);
}
logError(error, errorInfo) {
// Sanitize error data before sending
const sanitizedError = {
message: error.message,
name: error.name,
timestamp: new Date().toISOString(),
userAgent: navigator.userAgent,
url: window.location.href
};
// Send to your error reporting service
fetch('/api/errors', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(sanitizedError)
});
}
render() {
if (this.state.hasError) {
return (
<div className="error-boundary">
<h2>Something went wrong</h2>
<p>We're sorry, but something unexpected happened.</p>
<button onClick={() => window.location.reload()}>
Reload Page
</button>
</div>
);
}
return this.props.children;
}
}
// 2. Secure Logging
class SecureLogger {
static log(level, message, data = {}) {
// Sanitize sensitive data
const sanitizedData = this.sanitizeData(data);
const logEntry = {
level,
message,
data: sanitizedData,
timestamp: new Date().toISOString(),
url: window.location.href,
userAgent: navigator.userAgent
};
// Don't log sensitive information
if (this.containsSensitiveData(logEntry)) {
logEntry.data = '[SENSITIVE DATA REDACTED]';
}
console.log(`[${level.toUpperCase()}]`, logEntry);
// Send to logging service if needed
if (level === 'error' || level === 'warn') {
this.sendToLoggingService(logEntry);
}
}
static sanitizeData(data) {
const sensitiveFields = ['password', 'token', 'secret', 'key', 'auth'];
const sanitized = { ...data };
sensitiveFields.forEach(field => {
if (sanitized[field]) {
sanitized[field] = '[REDACTED]';
}
});
return sanitized;
}
static containsSensitiveData(data) {
const sensitivePatterns = [
/password/i,
/token/i,
/secret/i,
/key/i,
/auth/i,
/credit.?card/i,
/ssn/i
];
const dataString = JSON.stringify(data);
return sensitivePatterns.some(pattern => pattern.test(dataString));
}
static sendToLoggingService(logEntry) {
// Send to your logging service
fetch('/api/logs', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(logEntry)
});
}
}Security Testing & Monitoring
Security Testing Tools
// 1. Security Scanner
class SecurityScanner {
static scanForVulnerabilities() {
const vulnerabilities = [];
// Check for XSS vulnerabilities
if (this.hasInnerHTMLUsage()) {
vulnerabilities.push({
type: 'XSS',
severity: 'HIGH',
description: 'innerHTML usage detected',
recommendation: 'Use textContent instead'
});
}
// Check for eval usage
if (this.hasEvalUsage()) {
vulnerabilities.push({
type: 'Code Injection',
severity: 'CRITICAL',
description: 'eval() usage detected',
recommendation: 'Avoid eval() completely'
});
}
// Check for insecure protocols
if (this.hasInsecureProtocols()) {
vulnerabilities.push({
type: 'Insecure Protocol',
severity: 'MEDIUM',
description: 'HTTP usage detected',
recommendation: 'Use HTTPS only'
});
}
return vulnerabilities;
}
static hasInnerHTMLUsage() {
// Scan code for innerHTML usage
const scripts = document.querySelectorAll('script');
return Array.from(scripts).some(script =>
script.textContent.includes('innerHTML')
);
}
static hasEvalUsage() {
const scripts = document.querySelectorAll('script');
return Array.from(scripts).some(script =>
script.textContent.includes('eval(')
);
}
static hasInsecureProtocols() {
return window.location.protocol === 'http:';
}
}
// 2. Security Monitoring
class SecurityMonitor {
constructor() {
this.suspiciousActivities = [];
this.setupMonitoring();
}
setupMonitoring() {
// Monitor for suspicious DOM changes
this.observeDOM();
// Monitor for suspicious network requests
this.interceptRequests();
// Monitor for suspicious user interactions
this.monitorUserActions();
}
observeDOM() {
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'childList') {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
this.checkForSuspiciousContent(node);
}
});
}
});
});
observer.observe(document.body, {
childList: true,
subtree: true
});
}
checkForSuspiciousContent(element) {
const suspiciousPatterns = [
/<script/i,
/javascript:/i,
/on\w+=/i
];
const content = element.outerHTML;
suspiciousPatterns.forEach(pattern => {
if (pattern.test(content)) {
this.reportSuspiciousActivity('Suspicious content detected', {
pattern: pattern.source,
content: content.substring(0, 100)
});
}
});
}
interceptRequests() {
const originalFetch = window.fetch;
window.fetch = (...args) => {
const [url, options] = args;
// Check for suspicious requests
if (this.isSuspiciousRequest(url, options)) {
this.reportSuspiciousActivity('Suspicious request detected', {
url,
method: options?.method || 'GET'
});
}
return originalFetch(...args);
};
}
isSuspiciousRequest(url, options) {
// Check for requests to suspicious domains
const suspiciousDomains = ['malicious-site.com', 'phishing-site.com'];
return suspiciousDomains.some(domain => url.includes(domain));
}
monitorUserActions() {
// Monitor for rapid clicking (potential bot activity)
let clickCount = 0;
let lastClickTime = 0;
document.addEventListener('click', (e) => {
const now = Date.now();
if (now - lastClickTime < 100) { // Less than 100ms between clicks
clickCount++;
if (clickCount > 10) {
this.reportSuspiciousActivity('Rapid clicking detected', {
clickCount,
timeSpan: now - lastClickTime
});
}
} else {
clickCount = 1;
}
lastClickTime = now;
});
}
reportSuspiciousActivity(type, data) {
const activity = {
type,
data,
timestamp: new Date().toISOString(),
url: window.location.href,
userAgent: navigator.userAgent
};
this.suspiciousActivities.push(activity);
// Send to security monitoring service
fetch('/api/security/alert', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(activity)
});
}
}Security Checklist
Development Phase
- Input Validation - Validate all user inputs
- Output Encoding - Encode all dynamic content
- Authentication - Implement secure authentication
- Authorization - Check permissions for all actions
- HTTPS Only - Use HTTPS in production
- Security Headers - Set appropriate security headers
- CSP - Implement Content Security Policy
- Error Handling - Don't expose sensitive information in errors
Testing Phase
- Security Testing - Run automated security tests
- Penetration Testing - Conduct manual security testing
- Code Review - Review code for security vulnerabilities
- Dependency Scanning - Scan for vulnerable dependencies
- Configuration Review - Review security configurations
Deployment Phase
- Environment Variables - Use secure environment variables
- Secrets Management - Properly manage secrets and keys
- Monitoring - Set up security monitoring and alerting
- Backup Security - Secure backup procedures
- Incident Response - Have incident response procedures
Security Metrics
| Metric | Description | Target |
|---|---|---|
| Vulnerability Count | Number of known vulnerabilities | 0 |
| Security Incidents | Number of security incidents | 0 |
| Patch Time | Time to patch critical vulnerabilities | < 24 hours |
| Security Training | Percentage of team with security training | 100% |
| Code Review Coverage | Percentage of code reviewed for security | 100% |
Incident Response
Security Incident Response Plan
class SecurityIncidentResponse {
static async handleIncident(incident) {
// 1. Identify and Assess
const severity = this.assessSeverity(incident);
// 2. Contain
await this.containIncident(incident);
// 3. Eradicate
await this.eradicateThreat(incident);
// 4. Recover
await this.recoverSystems(incident);
// 5. Learn
await this.learnFromIncident(incident);
}
static assessSeverity(incident) {
const severityLevels = {
CRITICAL: 'Immediate action required',
HIGH: 'Action required within 1 hour',
MEDIUM: 'Action required within 24 hours',
LOW: 'Action required within 1 week'
};
// Assess based on impact and scope
return incident.impact === 'data_breach' ? 'CRITICAL' : 'HIGH';
}
static async containIncident(incident) {
// Implement containment measures
switch (incident.type) {
case 'xss':
await this.containXSS(incident);
break;
case 'csrf':
await this.containCSRF(incident);
break;
case 'data_breach':
await this.containDataBreach(incident);
break;
}
}
static async containXSS(incident) {
// Disable user input temporarily
// Increase CSP restrictions
// Monitor for additional attacks
}
static async containCSRF(incident) {
// Regenerate CSRF tokens
// Increase token validation
// Monitor for additional attempts
}
static async containDataBreach(incident) {
// Isolate affected systems
// Revoke compromised credentials
// Notify affected users
}
}Final Thoughts
Security is not a one-time effort—it's an ongoing process that requires vigilance, education, and continuous improvement. Remember:
Key Principles:
- Defense in Depth - Multiple layers of security
- Principle of Least Privilege - Minimal necessary permissions
- Fail Securely - Graceful degradation without exposing vulnerabilities
- Security by Design - Build security into your application from the start
Best Practices:
- Stay Updated - Keep dependencies and security knowledge current
- Automate Security - Use automated tools for security testing
- Train Your Team - Regular security training for all developers
- Monitor Continuously - Real-time security monitoring and alerting
- Plan for Incidents - Have incident response procedures ready
Getting Started:
- Audit Your Application - Identify current security posture
- Implement Basic Security - Start with essential security measures
- Add Security Testing - Integrate security testing into your workflow
- Monitor and Improve - Continuously monitor and improve security
Remember: Security is everyone's responsibility
Next: XSS vs CORS - Learn the differences between Cross-Site Scripting and Cross-Origin Resource Sharing.