React Security and Best Practices

1. XSS Prevention and Input Sanitization

Attack Vector Risk Prevention Example
User Input in JSX XSS via script injection React automatically escapes JSX expressions {userInput} is safe - React escapes HTML
URL Parameters JavaScript execution via href Validate and sanitize URLs, block javascript: protocol Use allowlist for protocols (http:, https:, mailto:)
HTML Attributes Event handler injection Never use user input directly in event handlers Avoid: onClick={eval(userInput)}
innerHTML/outerHTML Direct HTML injection Use React rendering, avoid dangerouslySetInnerHTML Sanitize with DOMPurify if HTML needed
CSS Injection Style-based XSS Validate style values, use CSS-in-JS safely Avoid: style={{'{{'}}background: userInput}}
SVG Content Script tags in SVG Sanitize SVG content, validate sources Use SVG as components, not raw strings

Example: Safe rendering of user input

// ✅ SAFE - React automatically escapes
function UserComment({ comment }) {
  return <div>{comment.text}</div>; // HTML entities escaped
}

// ✅ SAFE - Using textContent
function DisplayText({ text }) {
  const ref = useRef<HTMLDivElement>(null);
  
  useEffect(() => {
    if (ref.current) {
      ref.current.textContent = text; // Safe, no HTML parsing
    }
  }, [text]);
  
  return <div ref={ref} />;
}

// ❌ UNSAFE - Direct href from user input
function UnsafeLink({ url }) {
  return <a href={url}>Click</a>; // Can be javascript:alert('XSS')
}

// ✅ SAFE - Validate URL protocol
function SafeLink({ url }) {
  const isValidUrl = (url: string) => {
    try {
      const parsed = new URL(url);
      return ['http:', 'https:', 'mailto:'].includes(parsed.protocol);
    } catch {
      return false;
    }
  };
  
  if (!isValidUrl(url)) {
    return <span>Invalid URL</span>;
  }
  
  return <a href={url} rel="noopener noreferrer">Click</a>;
}

Example: Input sanitization library

// Install DOMPurify for HTML sanitization
npm install dompurify
npm install --save-dev @types/dompurify

import DOMPurify from 'dompurify';

// Sanitize HTML content
function SanitizedHtml({ html }: { html: string }) {
  const sanitized = DOMPurify.sanitize(html, {
    ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],
    ALLOWED_ATTR: ['href', 'title', 'target'],
    ALLOW_DATA_ATTR: false,
  });
  
  return <div dangerouslySetInnerHTML={{ __html: sanitized }} />;
}

// Sanitize user input before storing
function CommentForm() {
  const [comment, setComment] = useState('');
  
  const handleSubmit = async (e: FormEvent) => {
    e.preventDefault();
    
    // Sanitize before sending to API
    const sanitized = DOMPurify.sanitize(comment, {
      ALLOWED_TAGS: ['b', 'i', 'em', 'strong'],
      ALLOWED_ATTR: [],
    });
    
    await saveComment(sanitized);
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <textarea 
        value={comment}
        onChange={(e) => setComment(e.target.value)}
        maxLength={500}
      />
      <button type="submit">Post Comment</button>
    </form>
  );
}

2. dangerouslySetInnerHTML Safe Usage

Risk Mitigation Best Practice Example
XSS Attacks Always sanitize HTML before rendering Use DOMPurify or similar library Sanitize all user-generated content
Script Injection Remove script tags and event handlers Configure strict sanitization rules Block <script>, onclick, onerror, etc.
Style Attacks Limit allowed CSS properties Whitelist safe CSS properties only Avoid expression(), url(), import
Data Exfiltration Block external resource loading CSP headers, sanitize img/link src Restrict to same-origin or trusted domains
iframe Injection Block iframe tags unless necessary Use sandbox attribute if needed Restrict iframe capabilities
Form Hijacking Block form tags in user content Sanitize or block form elements Prevent form submission hijacking

Example: Safe dangerouslySetInnerHTML usage

import DOMPurify from 'dompurify';

// ❌ NEVER DO THIS - Unsafe!
function UnsafeHtml({ html }) {
  return <div dangerouslySetInnerHTML={{ __html: html }} />;
}

// ✅ SAFE - Sanitized HTML
function SafeHtml({ html }: { html: string }) {
  const sanitized = useMemo(() => {
    return DOMPurify.sanitize(html, {
      ALLOWED_TAGS: [
        'h1', 'h2', 'h3', 'p', 'br', 'strong', 'em', 'u',
        'ul', 'ol', 'li', 'a', 'blockquote', 'code', 'pre'
      ],
      ALLOWED_ATTR: {
        'a': ['href', 'title', 'target', 'rel'],
        'img': ['src', 'alt', 'title', 'width', 'height']
      },
      ALLOW_DATA_ATTR: false,
      FORBID_TAGS: ['script', 'iframe', 'object', 'embed', 'form'],
      FORBID_ATTR: ['onerror', 'onload', 'onclick', 'onmouseover']
    });
  }, [html]);
  
  return <div dangerouslySetInnerHTML={{ __html: sanitized }} />;
}

// ✅ BETTER - Use React components instead
function MarkdownRenderer({ markdown }: { markdown: string }) {
  // Use a library like react-markdown
  return <ReactMarkdown>{markdown}</ReactMarkdown>;
}

// Safe HTML with hooks
function useSafeHtml(html: string) {
  return useMemo(() => {
    if (!html) return '';
    
    return DOMPurify.sanitize(html, {
      ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'a'],
      ALLOWED_ATTR: ['href'],
      KEEP_CONTENT: true,
    });
  }, [html]);
}

function RichTextDisplay({ content }: { content: string }) {
  const safeHtml = useSafeHtml(content);
  
  if (!content) {
    return <p>No content available</p>;
  }
  
  return (
    <div 
      className="rich-text-content"
      dangerouslySetInnerHTML={{ __html: safeHtml }}
    />
  );
}
Warning: Never use dangerouslySetInnerHTML with unsanitized user input. Always validate HTML source, sanitize with DOMPurify, use strict whitelist of tags/attributes, implement Content Security Policy, prefer React components over raw HTML, audit third-party HTML carefully.

3. Props Validation and Runtime Type Checking

Approach Tool Features Use Case
PropTypes prop-types package Runtime validation, custom validators, development warnings JavaScript projects, simple validation
TypeScript Built-in type system Compile-time checking, inference, strict mode Type-safe React apps, preferred approach
Zod Runtime zod schemas Runtime + compile-time, parsing, transformation API responses, form validation, external data
io-ts TypeScript runtime validation Type guards, decoders, reporters Complex validation, FP patterns
Yup Schema validation Object schemas, async validation, transforms Form validation, data validation
Custom Validation Manual checks Full control, specific business logic Complex validation requirements

Example: PropTypes validation

import PropTypes from 'prop-types';

function UserCard({ user, onEdit, isActive }) {
  return (
    <div className={isActive ? 'active' : ''}>
      <h3>{user.name}</h3>
      <p>{user.email}</p>
      <button onClick={onEdit}>Edit</button>
    </div>
  );
}

UserCard.propTypes = {
  user: PropTypes.shape({
    id: PropTypes.number.isRequired,
    name: PropTypes.string.isRequired,
    email: PropTypes.string.isRequired,
    avatar: PropTypes.string,
  }).isRequired,
  onEdit: PropTypes.func.isRequired,
  isActive: PropTypes.bool,
};

UserCard.defaultProps = {
  isActive: false,
};

// Custom validators
function EmailValidator(props, propName, componentName) {
  const email = props[propName];
  
  if (email && !/^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/.test(email)) {
    return new Error(
      `Invalid prop \`${propName}\` supplied to \`${componentName}\`. ` +
      `Expected a valid email address.`
    );
  }
}

function ContactForm({ email }) {
  return <input type="email" value={email} />;
}

ContactForm.propTypes = {
  email: EmailValidator
};

Example: TypeScript with runtime validation

import { z } from 'zod';

// Define schema
const UserSchema = z.object({
  id: z.number().positive(),
  name: z.string().min(1).max(100),
  email: z.string().email(),
  age: z.number().min(0).max(150).optional(),
  role: z.enum(['admin', 'user', 'guest']),
});

type User = z.infer<typeof UserSchema>;

// Component with TypeScript
interface UserCardProps {
  user: User;
  onEdit: (user: User) => void;
  isActive?: boolean;
}

function UserCard({ user, onEdit, isActive = false }: UserCardProps) {
  return (
    <div className={isActive ? 'active' : ''}>
      <h3>{user.name}</h3>
      <p>{user.email}</p>
      <button onClick={() => onEdit(user)}>Edit</button>
    </div>
  );
}

// Validate API response
async function fetchUser(id: number): Promise<User> {
  const response = await fetch(`/api/users/${id}`);
  const data = await response.json();
  
  // Runtime validation
  try {
    return UserSchema.parse(data);
  } catch (error) {
    if (error instanceof z.ZodError) {
      console.error('Invalid user data:', error.errors);
      throw new Error('Invalid user data from API');
    }
    throw error;
  }
}

// Safe parse without throwing
async function fetchUserSafe(id: number) {
  const response = await fetch(`/api/users/${id}`);
  const data = await response.json();
  
  const result = UserSchema.safeParse(data);
  
  if (!result.success) {
    return { error: result.error, data: null };
  }
  
  return { error: null, data: result.data };
}

4. Authentication and Authorization Patterns

Pattern Implementation Security Considerations Use Case
JWT Tokens Store in memory or httpOnly cookies Never store in localStorage, use short expiry, refresh tokens Stateless authentication, API access
Session Cookies Server-side sessions, httpOnly secure cookies CSRF protection, SameSite attribute, secure flag Traditional web apps, server-side auth
OAuth 2.0 / OIDC Third-party auth providers (Google, Auth0) PKCE flow for SPAs, validate state parameter Social login, enterprise SSO
Protected Routes Conditional rendering, redirect logic Server-side validation, check permissions Role-based access control
API Security Authorization headers, CORS configuration Validate on server, don't trust client API endpoint protection
MFA/2FA Time-based OTP, SMS, authenticator apps Backup codes, rate limiting, secure storage Enhanced security for sensitive ops

Example: JWT authentication with refresh tokens

// AuthContext.tsx
interface AuthContextType {
  user: User | null;
  login: (email: string, password: string) => Promise<void>;
  logout: () => void;
  isAuthenticated: boolean;
}

const AuthContext = createContext<AuthContextType | null>(null);

export function AuthProvider({ children }: { children: ReactNode }) {
  const [user, setUser] = useState<User | null>(null);
  const [accessToken, setAccessToken] = useState<string | null>(null);

  // Store access token in memory only
  const login = async (email: string, password: string) => {
    const response = await fetch('/api/auth/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      credentials: 'include', // Include httpOnly refresh token cookie
      body: JSON.stringify({ email, password }),
    });

    if (!response.ok) throw new Error('Login failed');

    const { user, accessToken } = await response.json();
    setUser(user);
    setAccessToken(accessToken); // Store in memory
  };

  const logout = async () => {
    await fetch('/api/auth/logout', {
      method: 'POST',
      credentials: 'include',
    });
    setUser(null);
    setAccessToken(null);
  };

  // Refresh access token
  useEffect(() => {
    const refreshToken = async () => {
      try {
        const response = await fetch('/api/auth/refresh', {
          method: 'POST',
          credentials: 'include', // Send httpOnly cookie
        });

        if (response.ok) {
          const { user, accessToken } = await response.json();
          setUser(user);
          setAccessToken(accessToken);
        }
      } catch (error) {
        console.error('Token refresh failed:', error);
      }
    };

    refreshToken();

    // Refresh before expiry (every 14 minutes for 15-minute tokens)
    const interval = setInterval(refreshToken, 14 * 60 * 1000);
    return () => clearInterval(interval);
  }, []);

  return (
    <AuthContext.Provider value={{ user, login, logout, isAuthenticated: !!user }}>
      {children}
    </AuthContext.Provider>
  );
}

export const useAuth = () => {
  const context = useContext(AuthContext);
  if (!context) throw new Error('useAuth must be used within AuthProvider');
  return context;
};

Example: Protected routes and role-based access

// ProtectedRoute.tsx
interface ProtectedRouteProps {
  children: ReactNode;
  requiredRole?: string[];
  redirectTo?: string;
}

function ProtectedRoute({ 
  children, 
  requiredRole, 
  redirectTo = '/login' 
}: ProtectedRouteProps) {
  const { user, isAuthenticated } = useAuth();
  const location = useLocation();

  if (!isAuthenticated) {
    // Redirect to login, preserve intended destination
    return <Navigate to={redirectTo} state={{ from: location }} replace />;
  }

  // Check role-based access
  if (requiredRole && !requiredRole.includes(user.role)) {
    return <Navigate to="/unauthorized" replace />;
  }

  return <>{children}</>;
}

// Usage in routes
function App() {
  return (
    <Routes>
      <Route path="/login" element={<Login />} />
      <Route path="/unauthorized" element={<Unauthorized />} />
      
      <Route
        path="/dashboard"
        element={
          <ProtectedRoute>
            <Dashboard />
          </ProtectedRoute>
        }
      />
      
      <Route
        path="/admin"
        element={
          <ProtectedRoute requiredRole={['admin']}>
            <AdminPanel />
          </ProtectedRoute>
        }
      />
    </Routes>
  );
}

// Secure API requests
function useSecureApi() {
  const { accessToken } = useAuth();

  const secureRequest = async (url: string, options: RequestInit = {}) => {
    const response = await fetch(url, {
      ...options,
      headers: {
        ...options.headers,
        'Authorization': `Bearer ${accessToken}`,
        'Content-Type': 'application/json',
      },
      credentials: 'include',
    });

    if (response.status === 401) {
      // Token expired, trigger logout
      throw new Error('Unauthorized');
    }

    return response;
  };

  return { secureRequest };
}

5. Secure State Management Practices

Risk Secure Practice Implementation Example
Sensitive Data in State Never store passwords, credit cards, SSN in state Use secure backend storage, tokenization Store tokens server-side, use references only
localStorage Risks Avoid storing auth tokens in localStorage Use httpOnly cookies or memory storage XSS can access localStorage but not httpOnly cookies
State Persistence Encrypt sensitive persisted state Use encryption libraries, secure key management Encrypt before saving to storage
Redux DevTools Disable in production or sanitize actions Conditionally enable, filter sensitive data Redact passwords, tokens from action logs
State Leakage Clear sensitive state on logout Reset stores, clear memory Unmount components, clear caches
Cross-Tab State Validate state across browser tabs Use BroadcastChannel for sync, validate auth Logout all tabs when one logs out

Example: Secure state management

// ❌ NEVER DO THIS - Insecure!
localStorage.setItem('authToken', token);
localStorage.setItem('password', password);
localStorage.setItem('creditCard', cardNumber);

// ✅ SECURE - In-memory only, httpOnly cookies for refresh
const AuthProvider = ({ children }) => {
  // Access token in memory only
  const [accessToken, setAccessToken] = useState<string | null>(null);
  
  // Refresh token in httpOnly cookie (set by server)
  // Cookie: refreshToken=xxx; HttpOnly; Secure; SameSite=Strict
  
  return (
    <AuthContext.Provider value={{ accessToken }}>
      {children}
    </AuthContext.Provider>
  );
};

// Secure state persistence
import CryptoJS from 'crypto-js';

function useSecureStorage(key: string) {
  const encryptionKey = process.env.REACT_APP_ENCRYPTION_KEY;
  
  const setSecureItem = (value: any) => {
    const encrypted = CryptoJS.AES.encrypt(
      JSON.stringify(value),
      encryptionKey
    ).toString();
    
    localStorage.setItem(key, encrypted);
  };
  
  const getSecureItem = () => {
    const encrypted = localStorage.getItem(key);
    if (!encrypted) return null;
    
    try {
      const decrypted = CryptoJS.AES.decrypt(encrypted, encryptionKey);
      return JSON.parse(decrypted.toString(CryptoJS.enc.Utf8));
    } catch {
      return null;
    }
  };
  
  const removeSecureItem = () => {
    localStorage.removeItem(key);
  };
  
  return { setSecureItem, getSecureItem, removeSecureItem };
}

// Redux DevTools - sanitize sensitive data
const store = configureStore({
  reducer: rootReducer,
  devTools: process.env.NODE_ENV !== 'production' && {
    actionSanitizer: (action) => ({
      ...action,
      payload: action.type.includes('PASSWORD') 
        ? '***REDACTED***' 
        : action.payload,
    }),
    stateSanitizer: (state) => ({
      ...state,
      auth: {
        ...state.auth,
        password: '***REDACTED***',
        token: '***REDACTED***',
      },
    }),
  },
});

// Clear state on logout
function useSecureLogout() {
  const dispatch = useDispatch();
  const queryClient = useQueryClient();
  
  const logout = useCallback(async () => {
    // Clear Redux state
    dispatch({ type: 'RESET_STATE' });
    
    // Clear React Query cache
    queryClient.clear();
    
    // Clear any local storage (except user preferences)
    const preferences = localStorage.getItem('userPreferences');
    localStorage.clear();
    if (preferences) {
      localStorage.setItem('userPreferences', preferences);
    }
    
    // Call logout API
    await fetch('/api/auth/logout', {
      method: 'POST',
      credentials: 'include',
    });
    
    // Redirect to login
    window.location.href = '/login';
  }, [dispatch, queryClient]);
  
  return logout;
}

6. Content Security Policy with React Apps

CSP Directive Purpose React Configuration Example Value
default-src Fallback for other directives Set restrictive default, override as needed 'self'
script-src Control script execution Use nonce or hash for inline scripts 'self' 'nonce-xyz123'
style-src Control stylesheet loading Use nonce for CSS-in-JS libraries 'self' 'unsafe-inline' (use nonce instead)
img-src Control image sources Whitelist CDNs and data URIs if needed 'self' data: https://cdn.example.com
connect-src Control fetch/XHR destinations Whitelist API endpoints 'self' https://api.example.com
frame-ancestors Prevent clickjacking Control iframe embedding 'none' or 'self'
upgrade-insecure-requests Auto upgrade HTTP to HTTPS Enable for production No value needed (directive only)

Example: CSP header configuration

// Next.js - next.config.js
const securityHeaders = [
  {
    key: 'Content-Security-Policy',
    value: [
      "default-src 'self'",
      "script-src 'self' 'unsafe-eval' 'unsafe-inline'", // Adjust for production
      "style-src 'self' 'unsafe-inline'",
      "img-src 'self' data: https:",
      "font-src 'self' data:",
      "connect-src 'self' https://api.example.com",
      "frame-ancestors 'none'",
      "base-uri 'self'",
      "form-action 'self'",
      "upgrade-insecure-requests",
    ].join('; '),
  },
  {
    key: 'X-Frame-Options',
    value: 'DENY',
  },
  {
    key: 'X-Content-Type-Options',
    value: 'nosniff',
  },
  {
    key: 'Referrer-Policy',
    value: 'strict-origin-when-cross-origin',
  },
  {
    key: 'Permissions-Policy',
    value: 'camera=(), microphone=(), geolocation=()',
  },
];

module.exports = {
  async headers() {
    return [
      {
        source: '/:path*',
        headers: securityHeaders,
      },
    ];
  },
};

// Express.js backend
import helmet from 'helmet';

app.use(
  helmet.contentSecurityPolicy({
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'", "'unsafe-inline'"], // Use nonce in production
      styleSrc: ["'self'", "'unsafe-inline'"],
      imgSrc: ["'self'", "data:", "https:"],
      connectSrc: ["'self'", "https://api.example.com"],
      fontSrc: ["'self'", "data:"],
      objectSrc: ["'none'"],
      mediaSrc: ["'self'"],
      frameSrc: ["'none'"],
      frameAncestors: ["'none'"],
      baseUri: ["'self'"],
      formAction: ["'self'"],
      upgradeInsecureRequests: [],
    },
  })
);

Example: Using CSP nonces with React

// Server-side (Express with React SSR)
import crypto from 'crypto';
import { renderToString } from 'react-dom/server';

app.get('*', (req, res) => {
  // Generate nonce for this request
  const nonce = crypto.randomBytes(16).toString('base64');
  
  // Set CSP header with nonce
  res.setHeader(
    'Content-Security-Policy',
    `script-src 'self' 'nonce-${nonce}'; style-src 'self' 'nonce-${nonce}'`
  );
  
  // Render app with nonce
  const html = renderToString(<App nonce={nonce} />);
  
  res.send(`
    <!DOCTYPE html>
    <html>
      <head>
        <style nonce="${nonce}">
          /* Critical CSS */
        </style>
      </head>
      <body>
        <div id="root">${html}</div>
        <script nonce="${nonce}" src="/static/bundle.js"></script>
      </body>
    </html>
  `);
});

// React component using nonce
function App({ nonce }) {
  return (
    <HelmetProvider>
      <Helmet>
        <script nonce={nonce}>
          {`window.__INITIAL_STATE__ = ${JSON.stringify(initialState)};`}
        </script>
      </Helmet>
      <YourApp />
    </HelmetProvider>
  );
}

// Strict CSP for production
const productionCSP = {
  defaultSrc: ["'none'"],
  scriptSrc: ["'self'"],
  styleSrc: ["'self'"],
  imgSrc: ["'self'", "data:", "https://trusted-cdn.com"],
  fontSrc: ["'self'"],
  connectSrc: ["'self'", "https://api.example.com"],
  frameSrc: ["'none'"],
  objectSrc: ["'none'"],
  baseUri: ["'self'"],
  formAction: ["'self'"],
  frameAncestors: ["'none'"],
  upgradeInsecureRequests: [],
};
CSP Best Practices: Start with strict policy, test thoroughly before production, avoid 'unsafe-inline' and 'unsafe-eval', use nonces or hashes for inline scripts, whitelist specific domains not wildcards, implement CSP reporting endpoint, monitor violations, update policy as app evolves, test in report-only mode first.

React Security Best Practices Summary