React Security JavaScript Frontend XSS CSRF

React Security Vulnerabilities: Common Attacks and Defenses

Learn about the most critical security vulnerabilities in React applications and how to protect against them.

By Vicente Aguilar January 20, 2024
15 min read

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>
  );
};
// ✅ 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

  1. Sanitize all user inputs - Use DOMPurify and html-escaper
  2. Implement CSRF protection - Use tokens and SameSite cookies
  3. Use httpOnly cookies - Store tokens securely
  4. Validate on client and server - Never trust client-side validation
  5. Implement CSP headers - Prevent XSS attacks
  6. Use HTTPS only - Never send sensitive data over HTTP
  7. Regular security audits - Use tools like OWASP ZAP
  8. Keep dependencies updated - Regularly update packages
  9. Implement proper error handling - Don’t expose sensitive information
  10. 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.

Resources

Share article