Security Best Practices
Essential security measures for React applications.
Input Sanitization
HTML Sanitization
import DOMPurify from 'dompurify';
// Sanitize user-generated HTML
const SafeHTML = ({ html }: { html: string }) => {
const sanitized = DOMPurify.sanitize(html, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p'],
ALLOWED_ATTR: ['href', 'target'],
});
return <div dangerouslySetInnerHTML={{ __html: sanitized }} />;
};URL Encoding
// Always encode user input in URLs
const searchUrl = `/search?q=${encodeURIComponent(userInput)}`;
// Validate URLs before using
const isValidUrl = (url: string): boolean => {
try {
const parsed = new URL(url);
return ['http:', 'https:'].includes(parsed.protocol);
} catch {
return false;
}
};Environment Variables
Client-Side Variables
# .env
# ✅ Client-side (VITE_ prefix) - Will be exposed
VITE_API_URL=https://api.example.com
VITE_PUBLIC_KEY=pk_live_xxx
# ❌ Never expose these to client
DATABASE_URL=postgres://...
API_SECRET=xxx
JWT_SECRET=xxxType-Safe Environment
// env.ts
import { z } from 'zod';
const envSchema = z.object({
VITE_API_URL: z.string().url(),
VITE_PUBLIC_KEY: z.string(),
});
export const env = envSchema.parse({
VITE_API_URL: import.meta.env.VITE_API_URL,
VITE_PUBLIC_KEY: import.meta.env.VITE_PUBLIC_KEY,
});Authentication
Token Storage
// ❌ Never store in localStorage for sensitive apps
localStorage.setItem('token', token);
// ✅ Use httpOnly cookies (set by server)
// Or memory + refresh token pattern
// Zustand with memory storage
const useAuthStore = create<AuthState>((set) => ({
token: null, // In memory only
// ...
}));API Request Security
// Always use HTTPS
const API_URL = 'https://api.example.com';
// Include credentials for cookie-based auth
apiClient.defaults.withCredentials = true;
// Add CSRF token if needed
apiClient.interceptors.request.use((config) => {
const csrfToken = getCsrfToken();
if (csrfToken) {
config.headers['X-CSRF-Token'] = csrfToken;
}
return config;
});Content Security Policy
// next.config.js or vite.config.ts
const securityHeaders = [
{
key: 'Content-Security-Policy',
value: [
"default-src 'self'",
"script-src 'self' 'unsafe-inline'",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: https:",
"font-src 'self'",
"connect-src 'self' https://api.example.com",
].join('; '),
},
{
key: 'X-Frame-Options',
value: 'DENY',
},
{
key: 'X-Content-Type-Options',
value: 'nosniff',
},
];Dependency Security
# Regular security audits
pnpm audit
# Fix vulnerabilities
pnpm audit --fix
# Check for outdated packages
pnpm outdatedSecurity Checklist
- All user input is sanitized
- HTTPS only in production
- Environment variables properly scoped
- No secrets in client code
- CSP headers configured
- XSS protection (DOMPurify)
- CSRF protection
- Secure cookie settings
- Regular
pnpm audit - Dependencies updated
- Auth tokens handled securely
Common Vulnerabilities
XSS Prevention
// ❌ Dangerous
<div dangerouslySetInnerHTML={{ __html: userInput }} />
// ✅ Safe - escaped by React
<div>{userInput}</div>
// ✅ Safe - sanitized
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(userInput) }} />Prototype Pollution
// ❌ Dangerous
const merged = { ...defaults, ...userObject };
// ✅ Validate input structure
const validated = userSchema.parse(userObject);
const merged = { ...defaults, ...validated };Open Redirect
// ❌ Dangerous
window.location.href = userProvidedUrl;
// ✅ Validate redirect URL
const safeRedirect = (url: string) => {
const allowed = ['/', '/dashboard', '/profile'];
if (allowed.includes(url)) {
window.location.href = url;
}
};