Security in React applications is a crucial aspect of modern software development. In this lesson, we will explore comprehensive security practices, from basic XSS protections to advanced techniques compliant with the OWASP Top 10.
React protects against XSS by default through automatic escaping:
1// React automatically escapes these values
2function UserProfile({ userData }) {
3 return (
4 <div>
5 {/* Safe - React escapes content */}
6 <h1>{userData.name}</h1>
7 <p>{userData.description}</p>
8
9 {/* DANGEROUS - bypasses escaping */}
10 <div dangerouslySetInnerHTML={{ __html: userData.bio }} />
11 </div>
12 );
13}1// Safe components with validation
2function SafeHTMLRenderer({ content, allowedTags = [] }) {
3 // DOMPurify library for HTML sanitization
4 const cleanHTML = DOMPurify.sanitize(content, {
5 ALLOWED_TAGS: allowedTags,
6 ALLOWED_ATTR: ['href', 'title', 'alt', 'src'],
7 ALLOW_DATA_ATTR: false
8 });
9
10 return (
11 <div dangerouslySetInnerHTML={{ __html: cleanHTML }} />
12 );
13}
14
15// URL validation
16function SafeLink({ href, children, ...props }) {
17 const isValidURL = (url) => {
18 try {
19 const urlObj = new URL(url);
20 // Allow only safe protocols
21 return ['http:', 'https:', 'mailto:'].includes(urlObj.protocol);
22 } catch {
23 return false;
24 }
25 };
26
27 if (!isValidURL(href)) {
28 console.warn('Unsafe URL blocked:', href);
29 return <span>{children}</span>;
30 }
31
32 return (
33 <a
34 href={href}
35 {...props}
36 rel="noopener noreferrer" // Protection against tabnabbing
37 >
38 {children}
39 </a>
40 );
41}
42
43// Safe image rendering
44function SafeImage({ src, alt, ...props }) {
45 const [imageSrc, setImageSrc] = useState(null);
46 const [error, setError] = useState(false);
47
48 useEffect(() => {
49 // Validate image source
50 if (src && typeof src === 'string') {
51 try {
52 const url = new URL(src);
53 if (['http:', 'https:', 'data:'].includes(url.protocol)) {
54 setImageSrc(src);
55 } else {
56 setError(true);
57 }
58 } catch {
59 setError(true);
60 }
61 }
62 }, [src]);
63
64 if (error) {
65 return <div className="image-error">Invalid image source</div>;
66 }
67
68 return imageSrc ? (
69 <img
70 src={imageSrc}
71 alt={alt}
72 {...props}
73 onError={() => setError(true)}
74 />
75 ) : null;
76}1// CSP Header configuration
2const cspConfig = {
3 'default-src': ["'self'"],
4 'script-src': [
5 "'self'",
6 "'unsafe-inline'", // Only in development
7 'https://apis.google.com',
8 'https://cdn.jsdelivr.net'
9 ],
10 'style-src': [
11 "'self'",
12 "'unsafe-inline'", // For CSS-in-JS
13 'https://fonts.googleapis.com'
14 ],
15 'img-src': [
16 "'self'",
17 'data:',
18 'https:',
19 'blob:'
20 ],
21 'font-src': [
22 "'self'",
23 'https://fonts.gstatic.com'
24 ],
25 'connect-src': [
26 "'self'",
27 'https://api.example.com',
28 'wss://socket.example.com'
29 ],
30 'frame-ancestors': ["'none'"],
31 'form-action': ["'self'"],
32 'upgrade-insecure-requests': []
33};
34
35// Next.js CSP configuration
36// next.config.js
37const securityHeaders = [
38 {
39 key: 'Content-Security-Policy',
40 value: Object.entries(cspConfig)
41 .map(([directive, sources]) =>
42 `${directive} ${sources.join(' ')}`
43 )
44 .join('; ')
45 },
46 {
47 key: 'X-Frame-Options',
48 value: 'DENY'
49 },
50 {
51 key: 'X-Content-Type-Options',
52 value: 'nosniff'
53 },
54 {
55 key: 'Referrer-Policy',
56 value: 'strict-origin-when-cross-origin'
57 },
58 {
59 key: 'Permissions-Policy',
60 value: 'camera=(), microphone=(), geolocation=()'
61 }
62];
63
64module.exports = {
65 async headers() {
66 return [
67 {
68 source: '/(.*)',
69 headers: securityHeaders
70 }
71 ];
72 }
73};1// Secure JWT storage and handling
2class SecureTokenManager {
3 constructor() {
4 this.tokenKey = 'auth_token';
5 this.refreshTokenKey = 'refresh_token';
6 }
7
8 // Storage in httpOnly cookies (best approach)
9 setTokens(accessToken, refreshToken) {
10 // Send to backend to set httpOnly cookies
11 fetch('/api/auth/set-tokens', {
12 method: 'POST',
13 headers: { 'Content-Type': 'application/json' },
14 body: JSON.stringify({ accessToken, refreshToken })
15 });
16 }
17
18 // Alternatively - sessionStorage (less secure)
19 setTokensInStorage(accessToken, refreshToken) {
20 // NEVER use localStorage for tokens!
21 sessionStorage.setItem(this.tokenKey, accessToken);
22
23 if (refreshToken) {
24 // Refresh token in a secure location
25 this.storeRefreshTokenSecurely(refreshToken);
26 }
27 }
28
29 getToken() {
30 return sessionStorage.getItem(this.tokenKey);
31 }
32
33 removeTokens() {
34 sessionStorage.removeItem(this.tokenKey);
35 sessionStorage.removeItem(this.refreshTokenKey);
36
37 // Also logout from cookies
38 fetch('/api/auth/logout', { method: 'POST' });
39 }
40
41 // Token validation
42 isTokenValid(token) {
43 if (!token) return false;
44
45 try {
46 const payload = JSON.parse(atob(token.split('.')[1]));
47 const now = Date.now() / 1000;
48
49 // Check if token has expired
50 return payload.exp > now;
51 } catch {
52 return false;
53 }
54 }
55
56 // Automatic token refresh
57 async refreshTokenIfNeeded() {
58 const token = this.getToken();
59
60 if (!token || !this.isTokenValid(token)) {
61 return await this.refreshToken();
62 }
63
64 return token;
65 }
66
67 async refreshToken() {
68 try {
69 const response = await fetch('/api/auth/refresh', {
70 method: 'POST',
71 credentials: 'include' // For httpOnly cookies
72 });
73
74 if (response.ok) {
75 const { accessToken } = await response.json();
76 sessionStorage.setItem(this.tokenKey, accessToken);
77 return accessToken;
78 }
79 } catch (error) {
80 console.error('Token refresh failed:', error);
81 this.removeTokens();
82 }
83
84 return null;
85 }
86}
87
88// Hook for authorization
89function useAuth() {
90 const [user, setUser] = useState(null);
91 const [loading, setLoading] = useState(true);
92 const tokenManager = useRef(new SecureTokenManager());
93
94 const login = async (credentials) => {
95 try {
96 const response = await fetch('/api/auth/login', {
97 method: 'POST',
98 headers: { 'Content-Type': 'application/json' },
99 body: JSON.stringify(credentials)
100 });
101
102 if (response.ok) {
103 const { user, accessToken, refreshToken } = await response.json();
104
105 // Secure token storage
106 tokenManager.current.setTokens(accessToken, refreshToken);
107 setUser(user);
108
109 return { success: true };
110 }
111 } catch (error) {
112 return { success: false, error: error.message };
113 }
114 };
115
116 const logout = () => {
117 tokenManager.current.removeTokens();
118 setUser(null);
119 };
120
121 // Automatic token refresh
122 useEffect(() => {
123 const interval = setInterval(async () => {
124 await tokenManager.current.refreshTokenIfNeeded();
125 }, 5 * 60 * 1000); // Every 5 minutes
126
127 return () => clearInterval(interval);
128 }, []);
129
130 return { user, login, logout, loading };
131}1// Secure Axios configuration
2const createSecureAxiosInstance = () => {
3 const tokenManager = new SecureTokenManager();
4
5 const axiosInstance = axios.create({
6 baseURL: process.env.REACT_APP_API_URL,
7 timeout: 10000,
8 withCredentials: true // For httpOnly cookies
9 });
10
11 // Request interceptor
12 axiosInstance.interceptors.request.use(
13 async (config) => {
14 const token = await tokenManager.refreshTokenIfNeeded();
15
16 if (token) {
17 config.headers.Authorization = `Bearer ${token}`;
18 }
19
20 // Add CSRF token if available
21 const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content;
22 if (csrfToken) {
23 config.headers['X-CSRF-Token'] = csrfToken;
24 }
25
26 return config;
27 },
28 (error) => Promise.reject(error)
29 );
30
31 // Response interceptor
32 axiosInstance.interceptors.response.use(
33 (response) => response,
34 async (error) => {
35 const originalRequest = error.config;
36
37 if (error.response?.status === 401 && !originalRequest._retry) {
38 originalRequest._retry = true;
39
40 const newToken = await tokenManager.refreshToken();
41
42 if (newToken) {
43 originalRequest.headers.Authorization = `Bearer ${newToken}`;
44 return axiosInstance(originalRequest);
45 } else {
46 // Redirect to login
47 window.location.href = '/login';
48 }
49 }
50
51 return Promise.reject(error);
52 }
53 );
54
55 return axiosInstance;
56};1// .env.example - template for the team
2REACT_APP_API_URL=https://api.example.com
3REACT_APP_ENVIRONMENT=development
4REACT_APP_SENTRY_DSN=your_sentry_dsn_here
5
6# Secrets (DO NOT ADD to .env.example)
7# API_SECRET_KEY=your_secret_here
8# DATABASE_URL=your_db_url_here1// Environment variable validation
2class EnvironmentValidator {
3 static requiredVars = [
4 'REACT_APP_API_URL',
5 'REACT_APP_ENVIRONMENT'
6 ];
7
8 static validate() {
9 const missing = this.requiredVars.filter(
10 varName => !process.env[varName]
11 );
12
13 if (missing.length > 0) {
14 throw new Error(
15 `Missing required environment variables: ${missing.join(', ')}`
16 );
17 }
18 }
19
20 static get(key, defaultValue = null) {
21 const value = process.env[key];
22
23 if (!value && defaultValue === null) {
24 console.warn(`Environment variable ${key} is not set`);
25 }
26
27 return value || defaultValue;
28 }
29
30 static getSecure(key) {
31 const value = process.env[key];
32
33 if (!value) {
34 throw new Error(`Secure environment variable ${key} is required`);
35 }
36
37 return value;
38 }
39}
40
41// Application configuration
42const appConfig = {
43 apiUrl: EnvironmentValidator.getSecure('REACT_APP_API_URL'),
44 environment: EnvironmentValidator.get('REACT_APP_ENVIRONMENT', 'development'),
45 isDevelopment: process.env.NODE_ENV === 'development',
46 isProduction: process.env.NODE_ENV === 'production',
47
48 // Helper functions
49 get isSecure() {
50 return this.apiUrl.startsWith('https://');
51 },
52
53 get shouldEnableDevTools() {
54 return this.isDevelopment && !this.isProduction;
55 }
56};
57
58// Validation at application startup
59EnvironmentValidator.validate();1// ConfigProvider.jsx
2const ConfigContext = createContext();
3
4export function ConfigProvider({ children }) {
5 const config = useMemo(() => {
6 const baseConfig = {
7 apiUrl: process.env.REACT_APP_API_URL,
8 environment: process.env.REACT_APP_ENVIRONMENT,
9 version: process.env.REACT_APP_VERSION || '1.0.0'
10 };
11
12 // Environment-specific configuration
13 const environmentConfigs = {
14 development: {
15 debug: true,
16 logLevel: 'debug',
17 enableMocks: true
18 },
19 staging: {
20 debug: true,
21 logLevel: 'info',
22 enableMocks: false
23 },
24 production: {
25 debug: false,
26 logLevel: 'error',
27 enableMocks: false
28 }
29 };
30
31 return {
32 ...baseConfig,
33 ...environmentConfigs[baseConfig.environment] || environmentConfigs.production
34 };
35 }, []);
36
37 return (
38 <ConfigContext.Provider value={config}>
39 {children}
40 </ConfigContext.Provider>
41 );
42}
43
44export const useConfig = () => {
45 const context = useContext(ConfigContext);
46 if (!context) {
47 throw new Error('useConfig must be used within ConfigProvider');
48 }
49 return context;
50};1// package.json scripts for security
2{
3 "scripts": {
4 "audit": "npm audit",
5 "audit:fix": "npm audit fix",
6 "audit:force": "npm audit fix --force",
7 "security:check": "npm audit && npm run license:check",
8 "license:check": "license-checker --summary",
9 "deps:update": "npm-check-updates -u",
10 "deps:check": "npm outdated"
11 },
12 "devDependencies": {
13 "license-checker": "^25.0.1",
14 "npm-check-updates": "^16.0.0",
15 "audit-ci": "^6.6.1"
16 }
17}1# .github/workflows/security.yml
2name: Security Checks
3
4on: [push, pull_request]
5
6jobs:
7 security:
8 runs-on: ubuntu-latest
9
10 steps:
11 - uses: actions/checkout@v3
12
13 - name: Setup Node.js
14 uses: actions/setup-node@v3
15 with:
16 node-version: '18'
17 cache: 'npm'
18
19 - name: Install dependencies
20 run: npm ci
21
22 - name: Run npm audit
23 run: npm audit --audit-level high
24
25 - name: Check for known vulnerabilities
26 run: npx audit-ci --high
27
28 - name: License compliance check
29 run: npm run license:check
30
31 - name: SAST with CodeQL
32 uses: github/codeql-action/analyze@v2
33 with:
34 languages: javascript
35
36 - name: Dependency Review
37 uses: actions/dependency-review-action@v3
38 if: github.event_name == 'pull_request'1// SecurityMonitor.jsx
2class SecurityMonitor {
3 constructor() {
4 this.violations = [];
5 this.init();
6 }
7
8 init() {
9 // Monitor CSP violations
10 document.addEventListener('securitypolicyviolation', (e) => {
11 this.reportViolation({
12 type: 'CSP_VIOLATION',
13 directive: e.violatedDirective,
14 blockedURI: e.blockedURI,
15 documentURI: e.documentURI,
16 timestamp: new Date().toISOString()
17 });
18 });
19
20 // Monitor console.error for potential security issues
21 const originalError = console.error;
22 console.error = (...args) => {
23 const message = args.join(' ');
24
25 if (this.isSecurityRelated(message)) {
26 this.reportViolation({
27 type: 'SECURITY_ERROR',
28 message,
29 timestamp: new Date().toISOString()
30 });
31 }
32
33 originalError.apply(console, args);
34 };
35 }
36
37 isSecurityRelated(message) {
38 const securityKeywords = [
39 'unsafe-inline',
40 'unsafe-eval',
41 'xss',
42 'csrf',
43 'cors',
44 'unauthorized',
45 'forbidden'
46 ];
47
48 return securityKeywords.some(keyword =>
49 message.toLowerCase().includes(keyword)
50 );
51 }
52
53 reportViolation(violation) {
54 this.violations.push(violation);
55
56 // Send to monitoring system
57 if (process.env.NODE_ENV === 'production') {
58 fetch('/api/security/violations', {
59 method: 'POST',
60 headers: { 'Content-Type': 'application/json' },
61 body: JSON.stringify(violation)
62 }).catch(error => {
63 console.error('Failed to report security violation:', error);
64 });
65 }
66 }
67
68 getViolations() {
69 return [...this.violations];
70 }
71}
72
73// Hook for security monitoring
74export function useSecurityMonitor() {
75 const monitor = useRef(new SecurityMonitor());
76
77 const reportCustomViolation = useCallback((violation) => {
78 monitor.current.reportViolation({
79 type: 'CUSTOM_VIOLATION',
80 ...violation,
81 timestamp: new Date().toISOString()
82 });
83 }, []);
84
85 return {
86 reportViolation: reportCustomViolation,
87 getViolations: () => monitor.current.getViolations()
88 };
89}1// InputValidator.js
2class InputValidator {
3 static patterns = {
4 email: /^[^s@]+@[^s@]+.[^s@]+$/,
5 phone: /^+?[ds-()]+$/,
6 url: /^https?://.+/,
7 alphanumeric: /^[a-zA-Z0-9]+$/,
8 noSpecialChars: /^[a-zA-Z0-9s-_]+$/
9 };
10
11 static validate(value, rules) {
12 const errors = [];
13
14 // Required validation
15 if (rules.required && (!value || value.trim() === '')) {
16 errors.push('This field is required');
17 }
18
19 if (!value) return errors;
20
21 // Length validation
22 if (rules.minLength && value.length < rules.minLength) {
23 errors.push(`Minimum length is ${rules.minLength} characters`);
24 }
25
26 if (rules.maxLength && value.length > rules.maxLength) {
27 errors.push(`Maximum length is ${rules.maxLength} characters`);
28 }
29
30 // Pattern validation
31 if (rules.pattern && !this.patterns[rules.pattern]?.test(value)) {
32 errors.push(`Invalid ${rules.pattern} format`);
33 }
34
35 // Custom validation
36 if (rules.custom) {
37 const customError = rules.custom(value);
38 if (customError) errors.push(customError);
39 }
40
41 // XSS prevention
42 if (rules.preventXSS && this.containsPotentialXSS(value)) {
43 errors.push('Input contains potentially dangerous content');
44 }
45
46 return errors;
47 }
48
49 static containsPotentialXSS(value) {
50 const xssPatterns = [
51 /<script[^>]*>.*?</script>/gi,
52 /javascript:/gi,
53 /onw+s*=/gi,
54 /<iframe[^>]*>/gi,
55 /<object[^>]*>/gi,
56 /<embed[^>]*>/gi,
57 /evals*(/gi,
58 /expressions*(/gi
59 ];
60
61 return xssPatterns.some(pattern => pattern.test(value));
62 }
63
64 static sanitize(value, options = {}) {
65 if (!value) return value;
66
67 let sanitized = value;
68
69 // Remove HTML tags
70 if (options.stripHTML) {
71 sanitized = sanitized.replace(/<[^>]*>/g, '');
72 }
73
74 // Escape HTML entities
75 if (options.escapeHTML) {
76 sanitized = sanitized
77 .replace(/&/g, '&')
78 .replace(/</g, '<')
79 .replace(/>/g, '>')
80 .replace(/"/g, '"')
81 .replace(/'/g, ''');
82 }
83
84 // Remove null bytes
85 sanitized = sanitized.replace(/�/g, '');
86
87 // Trim whitespace
88 if (options.trim !== false) {
89 sanitized = sanitized.trim();
90 }
91
92 return sanitized;
93 }
94}
95
96// SecureForm component
97function SecureForm({ onSubmit, children }) {
98 const [formData, setFormData] = useState({});
99 const [errors, setErrors] = useState({});
100 const [isSubmitting, setIsSubmitting] = useState(false);
101
102 const validateField = useCallback((name, value, rules) => {
103 const fieldErrors = InputValidator.validate(value, rules);
104
105 setErrors(prev => ({
106 ...prev,
107 [name]: fieldErrors.length > 0 ? fieldErrors : undefined
108 }));
109
110 return fieldErrors.length === 0;
111 }, []);
112
113 const handleInputChange = useCallback((name, value, rules) => {
114 // Sanitize input
115 const sanitizedValue = InputValidator.sanitize(value, {
116 stripHTML: true,
117 escapeHTML: true,
118 trim: true
119 });
120
121 setFormData(prev => ({
122 ...prev,
123 [name]: sanitizedValue
124 }));
125
126 // Validate on change
127 validateField(name, sanitizedValue, rules);
128 }, [validateField]);
129
130 const handleSubmit = async (e, validationRules) => {
131 e.preventDefault();
132 setIsSubmitting(true);
133
134 // Validate all fields
135 let isValid = true;
136 const newErrors = {};
137
138 Object.entries(validationRules).forEach(([fieldName, rules]) => {
139 const fieldValue = formData[fieldName];
140 const fieldErrors = InputValidator.validate(fieldValue, rules);
141
142 if (fieldErrors.length > 0) {
143 newErrors[fieldName] = fieldErrors;
144 isValid = false;
145 }
146 });
147
148 setErrors(newErrors);
149
150 if (isValid) {
151 try {
152 await onSubmit(formData);
153 } catch (error) {
154 console.error('Form submission failed:', error);
155 }
156 }
157
158 setIsSubmitting(false);
159 };
160
161 return (
162 <form onSubmit={handleSubmit}>
163 {React.Children.map(children, child => {
164 if (React.isValidElement(child) && child.props.name) {
165 return React.cloneElement(child, {
166 value: formData[child.props.name] || '',
167 onChange: (e) => handleInputChange(
168 child.props.name,
169 e.target.value,
170 child.props.validationRules
171 ),
172 error: errors[child.props.name],
173 disabled: isSubmitting
174 });
175 }
176 return child;
177 })}
178 </form>
179 );
180}1// OWASP Security Checklist Implementation
2
3// 1. Injection Prevention
4const SQLInjectionPrevention = {
5 // Always use prepared statements
6 safeQuery: (query, params) => {
7 // Backend implementation example
8 return db.prepare(query).run(params);
9 },
10
11 // Parameter validation
12 validateParams: (params, schema) => {
13 return schema.validate(params);
14 }
15};
16
17// 2. Broken Authentication Prevention
18const AuthenticationSecurity = {
19 // Strong passwords
20 passwordPolicy: {
21 minLength: 12,
22 requireUppercase: true,
23 requireLowercase: true,
24 requireNumbers: true,
25 requireSpecialChars: true,
26 maxAge: 90 * 24 * 60 * 60 * 1000 // 90 days
27 },
28
29 // Account lockout
30 loginAttempts: {
31 maxAttempts: 5,
32 lockoutDuration: 15 * 60 * 1000 // 15 minutes
33 },
34
35 // Session management
36 sessionConfig: {
37 httpOnly: true,
38 secure: true,
39 sameSite: 'strict',
40 maxAge: 30 * 60 * 1000 // 30 minutes
41 }
42};
43
44// 3. Sensitive Data Exposure Prevention
45const DataProtection = {
46 // Encrypt sensitive data
47 encryptSensitiveData: (data) => {
48 // Encryption implementation
49 return CryptoJS.AES.encrypt(data, process.env.ENCRYPTION_KEY);
50 },
51
52 // Secure storage
53 secureStorage: {
54 set: (key, value) => {
55 const encrypted = DataProtection.encryptSensitiveData(value);
56 sessionStorage.setItem(key, encrypted);
57 },
58
59 get: (key) => {
60 const encrypted = sessionStorage.getItem(key);
61 if (encrypted) {
62 const decrypted = CryptoJS.AES.decrypt(encrypted, process.env.ENCRYPTION_KEY);
63 return decrypted.toString(CryptoJS.enc.Utf8);
64 }
65 return null;
66 }
67 }
68};
69
70// 4. XML External Entities (XXE) Prevention
71const XXEPrevention = {
72 // Safe XML parsing
73 parseXMLSafely: (xmlString) => {
74 const parser = new DOMParser();
75 const doc = parser.parseFromString(xmlString, 'text/xml');
76
77 // Check for parsing errors
78 const parseError = doc.querySelector('parsererror');
79 if (parseError) {
80 throw new Error('Invalid XML format');
81 }
82
83 return doc;
84 }
85};
86
87// 5. Broken Access Control Prevention
88function ProtectedRoute({ children, requiredRole, requiredPermissions }) {
89 const { user, hasRole, hasPermissions } = useAuth();
90
91 if (!user) {
92 return <Redirect to="/login" />;
93 }
94
95 if (requiredRole && !hasRole(requiredRole)) {
96 return <div>Access Denied: Insufficient role</div>;
97 }
98
99 if (requiredPermissions && !hasPermissions(requiredPermissions)) {
100 return <div>Access Denied: Insufficient permissions</div>;
101 }
102
103 return children;
104}
105
106// 6. Security Misconfiguration Prevention
107const SecurityConfig = {
108 // Security headers
109 securityHeaders: {
110 'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',
111 'X-Content-Type-Options': 'nosniff',
112 'X-Frame-Options': 'DENY',
113 'X-XSS-Protection': '1; mode=block',
114 'Referrer-Policy': 'strict-origin-when-cross-origin'
115 },
116
117 // CORS configuration
118 corsConfig: {
119 origin: process.env.ALLOWED_ORIGINS?.split(',') || [],
120 credentials: true,
121 optionsSuccessStatus: 200
122 }
123};Comprehensive security practices in React require a holistic approach covering all application layers - from client to server. The key is regular auditing, updating dependencies, and following industry best practices compliant with the OWASP Top 10.