Introduction

Welcome to Chapter 16! After exploring the theoretical foundations of web security, understanding attacker mindsets, and dissecting the OWASP Top 10, it’s time to get our hands dirty. In this chapter, we’re going to apply all that knowledge by building a secure frontend for a hypothetical e-commerce application using React. This isn’t just about making things work; it’s about making them work securely.

Why an e-commerce frontend? Because these applications handle sensitive user data, payment information, and authentication, making them prime targets for various attacks. By building one with security in mind from the ground up, you’ll gain invaluable practical experience in defending against common vulnerabilities. We’ll focus on client-side aspects, assuming a secure backend handles server-side logic and data storage.

Before we dive in, make sure you’re comfortable with React fundamentals (components, state, props, hooks) and have a basic understanding of RESTful APIs. We’ll be drawing heavily on concepts introduced in previous chapters, especially those related to authentication, Cross-Site Scripting (XSS), and Cross-Site Request Forgery (CSRF). Ready to build something awesome and secure? Let’s go!

Core Concepts for a Secure React Frontend

Building a secure frontend isn’t just about preventing specific attacks; it’s about adopting a secure mindset throughout the development process. Here are the core concepts we’ll focus on:

1. Secure Authentication Flow

Authentication is the gatekeeper of your application. On the frontend, this primarily involves handling user input for login, sending credentials to the backend, and securely managing the authentication token received in response.

  • Tokens: We’ll assume our backend uses JSON Web Tokens (JWTs) or similar session tokens. The crucial decision is where to store them. Storing sensitive tokens directly in localStorage is generally discouraged due to its vulnerability to XSS attacks, which could allow an attacker to steal the token. A more secure approach often involves using HttpOnly and Secure cookies, managed by the backend. For this project, we’ll simulate a secure token retrieval and usage, focusing on how the frontend uses the token, not necessarily where it’s stored.
  • Protected Routes: Once authenticated, users should only be able to access certain parts of your application. We’ll implement client-side route protection to guide users, but always remember that server-side authorization is the ultimate gatekeeper.

Let’s visualize a typical secure authentication flow:

sequenceDiagram participant User participant ReactApp participant BackendAPI User->>ReactApp: Enter Credentials (Username/Password) ReactApp->>BackendAPI: POST /login (Credentials) BackendAPI->>BackendAPI: Verify Credentials, Generate Token BackendAPI-->>ReactApp: Send Token (in HttpOnly, Secure cookie) ReactApp->>ReactApp: Store Authentication State ReactApp->>User: Redirect to Dashboard/Protected Area User->>ReactApp: Request Protected Resource ReactApp->>BackendAPI: GET /protected-data (Cookie sent automatically) BackendAPI->>BackendAPI: Validate Token from Cookie BackendAPI-->>ReactApp: Return Protected Data ReactApp->>User: Display Protected Data

Explanation of the Diagram:

  1. User Enters Credentials: The user provides their username and password through a login form in the React application.
  2. React App Sends Credentials: The React app sends these credentials to the backend’s /login endpoint, typically via a POST request.
  3. Backend Verification and Token Generation: The backend verifies the credentials. If valid, it generates an authentication token (like a JWT or session ID).
  4. Backend Sends Token Securely: Instead of sending the token in the response body for localStorage storage, the backend sets it in an HttpOnly, Secure, and SameSite cookie.
    • HttpOnly: Prevents JavaScript from accessing the cookie, mitigating XSS token theft.
    • Secure: Ensures the cookie is only sent over HTTPS connections.
    • SameSite: Protects against CSRF attacks by controlling when the browser sends the cookie with cross-site requests.
  5. React App Stores Authentication State: The React app updates its internal state to reflect the user’s authenticated status. It doesn’t directly handle the token from the cookie, as the browser automatically manages it.
  6. Redirect to Protected Area: The user is redirected to a part of the application that requires authentication.
  7. User Requests Protected Resource: The user navigates to a protected page or performs an action requiring authentication.
  8. React App Makes API Call (Cookie Sent Automatically): When the React app makes an API call to a protected endpoint (e.g., /protected-data), the browser automatically includes the HttpOnly cookie with the request.
  9. Backend Validates Token: The backend receives the request, extracts the token from the cookie, and validates it.
  10. Backend Returns Protected Data: If the token is valid, the backend processes the request and returns the requested data.
  11. React App Displays Data: The React app receives and displays the data to the user.

2. XSS Prevention in React

Cross-Site Scripting (XSS) is a major concern. React, by default, offers strong protections against XSS by escaping content rendered in JSX. This means that if a user tries to inject <script> tags into a product review, React will render it as plain text &lt;script&gt; instead of executing it.

However, there’s a loophole: dangerouslySetInnerHTML. This prop allows you to insert raw HTML into a component. While sometimes necessary (e.g., displaying rich text from a trusted source), it bypasses React’s built-in escaping and opens the door to XSS if the content isn’t thoroughly sanitized.

Key Rule: Avoid dangerouslySetInnerHTML unless absolutely necessary, and always sanitize any untrusted input before using it with this prop.

3. CSRF Prevention

Cross-Site Request Forgery (CSRF) tricks authenticated users into executing unwanted actions. Since we’re relying on HttpOnly, Secure, and SameSite=Lax (or Strict) cookies, the browser’s SameSite policy provides a good first line of defense for most GET and safe POST requests.

For state-changing POST, PUT, DELETE requests that require stronger protection, the backend can issue a CSRF token. The frontend would then include this token in a custom HTTP header (e.g., X-CSRF-Token) or the request body for every relevant request. The backend verifies this token. This approach works because an attacker’s cross-site request generally cannot read the token from your domain or set a custom header.

4. Secure API Communication

  • HTTPS Everywhere: Always communicate with your backend over HTTPS. This encrypts data in transit, preventing eavesdropping and tampering.
  • Input Validation: While the backend is the authoritative source for validation, client-side validation provides a better user experience by catching errors early. Never rely solely on client-side validation for security.
  • Error Handling: Be careful not to leak sensitive information in error messages. Generic error messages are usually best for the client.

5. Secure State Management and Storage

  • Avoid Sensitive Data in Global State/Context: Try not to store highly sensitive data (like full payment card numbers) in your React component state or global context where it might be accidentally logged or exposed.
  • No Sensitive Data in localStorage/sessionStorage: As mentioned, these are vulnerable to XSS. Use HttpOnly cookies for authentication tokens. For non-sensitive, user-specific preferences, localStorage is acceptable.

Step-by-Step Implementation: Building Our Secure E-commerce Frontend

Let’s start building a simple React e-commerce frontend. We’ll set up a basic project, implement a simulated authentication flow, display products, and handle user input securely.

Step 1: Project Setup

We’ll use Vite, a modern build tool that offers a faster development experience than Create React App.

First, ensure you have Node.js (version 18.x or later recommended for 2026) installed.

Open your terminal and run:

npm create vite@latest my-secure-ecommerce -- --template react-ts

Explanation:

  • npm create vite@latest: This command uses npm to execute the latest vite package, which sets up a new Vite project.
  • my-secure-ecommerce: This will be the name of our project directory.
  • -- --template react-ts: This tells Vite to scaffold a project using the React template with TypeScript. Using TypeScript adds a layer of type safety, which can help prevent certain classes of errors.

Now, navigate into your project directory and install the dependencies:

cd my-secure-ecommerce
npm install

Let’s clean up the src/App.tsx file to start fresh.

Replace the content of src/App.tsx with:

// src/App.tsx
import React from 'react';
import './App.css'; // Assuming you have an App.css for basic styling

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <h1>Welcome to Our Secure E-commerce Store</h1>
        <p>Your shopping experience, secured.</p>
      </header>
      <main>
        {/* We'll add our components here */}
      </main>
    </div>
  );
}

export default App;

Explanation: This is a minimal React component, serving as the root of our application. We’ve added a simple header to welcome users. The <main> tag is where we’ll place our secure components.

You can run your application to see it:

npm run dev

Open your browser to the URL provided (usually http://localhost:5173).

Step 2: Implementing a Simulated Secure Authentication

For this project, we’ll simulate the backend handling HttpOnly cookies for authentication. Our frontend will simply manage a boolean isAuthenticated state and simulate API calls.

First, let’s create a components folder inside src and add AuthContext.tsx and Login.tsx.

Create src/components/AuthContext.tsx:

// src/components/AuthContext.tsx
import React, { createContext, useState, useContext, ReactNode } from 'react';

// Define the shape of our authentication context
interface AuthContextType {
  isAuthenticated: boolean;
  login: (username: string, password: string) => Promise<boolean>;
  logout: () => void;
  user: { username: string } | null;
}

// Create the context with a default (null) value
const AuthContext = createContext<AuthContextType | undefined>(undefined);

// Define props for the AuthProvider
interface AuthProviderProps {
  children: ReactNode;
}

// Create the AuthProvider component
export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
  const [isAuthenticated, setIsAuthenticated] = useState<boolean>(false);
  const [user, setUser] = useState<{ username: string } | null>(null);

  // Simulate a login API call
  const login = async (username: string, password: string): Promise<boolean> => {
    // In a real app, this would be an actual fetch/axios call to your backend
    // The backend would set an HttpOnly, Secure cookie upon successful login
    console.log(`Attempting login for: ${username}`);
    if (username === 'user' && password === 'password') { // Dummy credentials
      // Simulate successful backend response (e.g., cookie set)
      setIsAuthenticated(true);
      setUser({ username });
      console.log('Login successful!');
      return true;
    }
    console.log('Login failed: Invalid credentials.');
    return false;
  };

  // Simulate logout
  const logout = () => {
    // In a real app, this might involve calling a backend /logout endpoint
    // to clear the HttpOnly cookie.
    setIsAuthenticated(false);
    setUser(null);
    console.log('Logged out.');
  };

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

// Custom hook to easily use the authentication context
export const useAuth = () => {
  const context = useContext(AuthContext);
  if (context === undefined) {
    throw new Error('useAuth must be used within an AuthProvider');
  }
  return context;
};

Explanation:

  • AuthContextType: Defines the structure of our authentication state and actions.
  • AuthContext: The actual React Context object.
  • AuthProvider: This component wraps our application (or parts of it) and provides the authentication state and functions (login, logout) to any child component that uses useAuth.
  • useState: Manages the isAuthenticated boolean and user object.
  • login function: This simulates a call to your backend. In a real scenario, a successful backend response would set an HttpOnly, Secure, SameSite cookie in the browser, and your frontend would simply update its isAuthenticated state based on the presence of that session (e.g., by making a /me endpoint call). For our demo, we just check dummy credentials.
  • logout function: Simulates logging out. A real logout might also involve an API call to invalidate the session on the backend.
  • useAuth hook: A convenient custom hook to access the context values within any component.

Now, let’s create a Login component.

Create src/components/Login.tsx:

// src/components/Login.tsx
import React, { useState } from 'react';
import { useAuth } from './AuthContext';

const Login: React.FC = () => {
  const [username, setUsername] = useState<string>('');
  const [password, setPassword] = useState<string>('');
  const [error, setError] = useState<string>('');
  const { login } = useAuth();

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setError(''); // Clear previous errors

    // Basic client-side validation for UX (server-side is for security!)
    if (!username || !password) {
      setError('Please enter both username and password.');
      return;
    }

    const success = await login(username, password);
    if (!success) {
      setError('Invalid username or password.');
    } else {
      // Login successful, context state updated, component will re-render
      // In a real app, you might redirect the user here using react-router-dom
      console.log('Login component: User successfully logged in.');
    }
  };

  return (
    <div className="login-container">
      <h2>Login</h2>
      <form onSubmit={handleSubmit}>
        <div>
          <label htmlFor="username">Username:</label>
          <input
            type="text"
            id="username"
            value={username}
            onChange={(e) => setUsername(e.target.value)}
            aria-label="Username"
            required
          />
        </div>
        <div>
          <label htmlFor="password">Password:</label>
          <input
            type="password"
            id="password"
            value={password}
            onChange={(e) => setPassword(e.target.value)}
            aria-label="Password"
            required
          />
        </div>
        {error && <p className="error-message" role="alert">{error}</p>}
        <button type="submit">Log In</button>
      </form>
    </div>
  );
};

export default Login;

Explanation:

  • useState: Manages the input field values (username, password) and any error messages.
  • useAuth(): Accesses the login function from our AuthContext.
  • handleSubmit:
    • Prevents default form submission.
    • Performs basic client-side validation for immediate user feedback. Crucially, this is for UX, not security. The backend must always validate credentials.
    • Calls the login function from AuthContext.
    • Sets an error message if login fails.
  • aria-label and role="alert": These are important for accessibility, informing screen readers about the purpose of inputs and the nature of error messages.

Now, let’s integrate our AuthProvider and Login component into App.tsx. We’ll also add a simple Dashboard component that’s only visible when authenticated.

Create src/components/Dashboard.tsx:

// src/components/Dashboard.tsx
import React from 'react';
import { useAuth } from './AuthContext';

const Dashboard: React.FC = () => {
  const { user, logout } = useAuth();

  return (
    <div className="dashboard-container">
      <h2>Welcome, {user?.username}!</h2>
      <p>This is your secure e-commerce dashboard.</p>
      <button onClick={logout}>Logout</button>
      {/* We'll add product listings and other features here */}
    </div>
  );
};

export default Dashboard;

Explanation: A simple component that greets the logged-in user and provides a logout button.

Update src/App.tsx:

// src/App.tsx
import React from 'react';
import './App.css'; // Assuming you have an App.css for basic styling
import { AuthProvider, useAuth } from './components/AuthContext';
import Login from './components/Login';
import Dashboard from './components/Dashboard';

function AppContent() {
  const { isAuthenticated } = useAuth();

  return (
    <div className="App">
      <header className="App-header">
        <h1>Welcome to Our Secure E-commerce Store</h1>
        <p>Your shopping experience, secured.</p>
      </header>
      <main>
        {isAuthenticated ? <Dashboard /> : <Login />}
      </main>
    </div>
  );
}

function App() {
  return (
    <AuthProvider>
      <AppContent />
    </AuthProvider>
  );
}

export default App;

Explanation:

  • We’ve wrapped our AppContent component with AuthProvider. This makes isAuthenticated and other auth functions available throughout our app.
  • AppContent now conditionally renders Dashboard if isAuthenticated is true, otherwise it shows the Login component. This is our basic client-side route protection.

Run npm run dev and test the login with username: user, password: password. You should see the login form disappear and the dashboard appear.

Step 3: Secure Product Listing and XSS Prevention

Let’s add a product listing feature. We’ll simulate fetching products from an API and display them, paying close attention to user-generated content like product descriptions or reviews.

Create src/components/ProductList.tsx:

// src/components/ProductList.tsx
import React, { useState, useEffect } from 'react';
import DOMPurify from 'dompurify'; // For sanitizing HTML

// Define product interface
interface Product {
  id: string;
  name: string;
  description: string; // Could contain user-generated content
  price: number;
}

const ProductList: React.FC = () => {
  const [products, setProducts] = useState<Product[]>([]);
  const [loading, setLoading] = useState<boolean>(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const fetchProducts = async () => {
      try {
        // Simulate an API call to a secure backend endpoint
        // In a real app, this would be: fetch('/api/products', { credentials: 'include' })
        // The browser would automatically send the HttpOnly cookie.
        const response = await new Promise<Product[]>(resolve =>
          setTimeout(() => resolve([
            { id: '1', name: 'Secure Laptop', description: 'A laptop with advanced security features. <script>alert("XSS attempt!");</script>', price: 1200 },
            { id: '2', name: 'Encrypted USB Drive', description: 'Keeps your data safe. <b>Strong encryption!</b>', price: 50 },
            { id: '3', name: 'Privacy Screen Filter', description: 'Protects your screen from prying eyes. Enjoy peace of mind.', price: 30 },
          ]), 500)
        );
        setProducts(response);
      } catch (err) {
        console.error('Failed to fetch products:', err);
        setError('Failed to load products. Please try again later.');
      } finally {
        setLoading(false);
      }
    };

    fetchProducts();
  }, []);

  if (loading) return <p>Loading products...</p>;
  if (error) return <p className="error-message">{error}</p>;

  return (
    <div className="product-list-container">
      <h3>Our Products</h3>
      <div className="products-grid">
        {products.map(product => (
          <div key={product.id} className="product-card">
            <h4>{product.name}</h4>
            <p>Price: ${product.price.toFixed(2)}</p>
            {/*
              CRITICAL SECURITY POINT:
              DO NOT use dangerouslySetInnerHTML directly with untrusted input.
              Always sanitize it first.
              React automatically escapes content in JSX, which is safer.
              But if you MUST render raw HTML (e.g., rich text), use a sanitizer.
            */}
            <p>Description:</p>
            <div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(product.description) }} />
            {/* If we just rendered {product.description} React would escape the script tag */}
            {/* <p>Description: {product.description}</p> */}
          </div>
        ))}
      </div>
    </div>
  );
};

export default ProductList;

Explanation:

  • Product Interface: Defines the expected structure of our product data.
  • useState, useEffect: Manage product data, loading state, and errors.
  • fetchProducts: Simulates an API call. Notice the simulated XSS attempt in product.description for the “Secure Laptop”.
  • DOMPurify: This is a crucial library for sanitizing HTML. Before using dangerouslySetInnerHTML, we pass the potentially untrusted product.description through DOMPurify.sanitize(). This function removes any malicious scripts or attributes, making the HTML safe to render.
    • Installation: You’ll need to install DOMPurify.
      npm install dompurify
      npm install --save-dev @types/dompurify # For TypeScript types
      
  • dangerouslySetInnerHTML: We use this after sanitization. If we just rendered {product.description} in JSX, React would automatically escape the HTML entities (< becomes &lt;, etc.), which is the safest default behavior. We use dangerouslySetInnerHTML here specifically to demonstrate the need for sanitization when dealing with rich text that might contain intended HTML (like <b>) alongside unintended malicious scripts.

Now, add the ProductList component to our Dashboard.

Update src/components/Dashboard.tsx:

// src/components/Dashboard.tsx
import React from 'react';
import { useAuth } from './AuthContext';
import ProductList from './ProductList'; // Import ProductList

const Dashboard: React.FC = () => {
  const { user, logout } = useAuth();

  return (
    <div className="dashboard-container">
      <h2>Welcome, {user?.username}!</h2>
      <p>This is your secure e-commerce dashboard.</p>
      <button onClick={logout}>Logout</button>

      <hr /> {/* Separator */}

      <ProductList /> {/* Add the ProductList here */}
    </div>
  );
};

export default Dashboard;

Now, log in (user/password) and observe the product list. The alert("XSS attempt!") should not pop up because DOMPurify has removed the script tag. The <b>Strong encryption!</b> text, however, should be bold because DOMPurify allows safe HTML tags by default.

Step 4: Handling User Input Securely (Search/Reviews)

Let’s imagine a search bar or a review submission form. Any user input that might be displayed back to other users must be treated as untrusted.

For a search bar, we’re typically sending the input to an API, not rendering it directly as HTML.

Create src/components/SearchBar.tsx:

// src/components/SearchBar.tsx
import React, { useState } from 'react';

interface SearchBarProps {
  onSearch: (query: string) => void;
}

const SearchBar: React.FC<SearchBarProps> = ({ onSearch }) => {
  const [query, setQuery] = useState<string>('');

  const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setQuery(e.target.value);
  };

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    // Client-side trimming for UX, backend must validate and sanitize!
    const trimmedQuery = query.trim();
    if (trimmedQuery) {
      onSearch(trimmedQuery);
    }
  };

  return (
    <form onSubmit={handleSubmit} className="search-bar">
      <input
        type="text"
        placeholder="Search for products..."
        value={query}
        onChange={handleInputChange}
        aria-label="Product search"
      />
      <button type="submit">Search</button>
    </form>
  );
};

export default SearchBar;

Explanation:

  • This is a standard search bar. The key security takeaway here is that query is just a string. When it’s passed to onSearch, it’s expected to be sent to a backend API.
  • The backend is responsible for:
    • Input Validation: Ensuring the query is of an expected format/length.
    • Sanitization: If the query is ever used in a context that could lead to XSS (e.g., reflected in an HTML page without proper encoding), it must be sanitized. For database queries, parameterized queries prevent SQL injection.

Let’s add SearchBar to our Dashboard and simulate a search.

Update src/components/Dashboard.tsx again:

// src/components/Dashboard.tsx
import React from 'react';
import { useAuth } from './AuthContext';
import ProductList from './ProductList';
import SearchBar from './SearchBar'; // Import SearchBar

const Dashboard: React.FC = () => {
  const { user, logout } = useAuth();

  const handleSearch = (query: string) => {
    console.log(`Searching for: "${query}". This query would be sent to the backend.`);
    // In a real application, you would make an API call here:
    // fetch(`/api/products?search=${encodeURIComponent(query)}`, { credentials: 'include' });
    // And then update the product list state.
    alert(`Simulated search for: "${query}". Backend would handle actual search securely.`);
  };

  return (
    <div className="dashboard-container">
      <h2>Welcome, {user?.username}!</h2>
      <p>This is your secure e-commerce dashboard.</p>
      <button onClick={logout}>Logout</button>

      <hr />

      <SearchBar onSearch={handleSearch} />

      <hr />

      <ProductList />
    </div>
  );
};

export default Dashboard;

Explanation:

  • The handleSearch function receives the user’s query.
  • We use encodeURIComponent(query) when constructing the simulated API URL. This is important for correctly encoding URL parameters and preventing certain types of attacks (like URL-based XSS if the backend reflects the unencoded query).

Step 5: Secure API Calls with CSRF Consideration

While our HttpOnly cookies already provide SameSite protection against basic CSRF, for critical state-changing operations (like adding to cart, checkout), a CSRF token is a robust defense.

Let’s simulate adding a product to a cart. We’ll assume the backend provides a CSRF token in a cookie (e.g., XSRF-TOKEN) or a meta tag on initial page load, and our frontend needs to include it in POST requests.

For this example, we’ll manually set a dummy CSRF token for demonstration. In a real app, you’d retrieve it from your backend.

Create src/components/AddToCartButton.tsx:

// src/components/AddToCartButton.tsx
import React from 'react';

interface AddToCartButtonProps {
  productId: string;
  productName: string;
}

const AddToCartButton: React.FC<AddToCartButtonProps> = ({ productId, productName }) => {
  // In a real application, you would fetch this token from your backend
  // e.g., from a meta tag in the initial HTML, or a specific API endpoint.
  // For demonstration, we'll use a dummy token.
  const csrfToken = 'dummy-csrf-token-from-backend-2026';

  const handleAddToCart = async () => {
    console.log(`Attempting to add product ${productName} (ID: ${productId}) to cart.`);

    try {
      // Simulate an API call to add to cart
      // Always include credentials to send HttpOnly cookies
      // Include CSRF token in a custom header for state-changing POST requests
      const response = await fetch('/api/cart/add', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'X-CSRF-Token': csrfToken, // Include the CSRF token
          // The browser automatically sends HttpOnly cookies if `credentials: 'include'` is used
        },
        body: JSON.stringify({ productId, quantity: 1 }),
        credentials: 'include', // Ensure cookies are sent with cross-origin requests (if necessary)
      });

      if (response.ok) {
        alert(`${productName} added to cart securely!`);
        console.log('Product added to cart successfully.');
      } else {
        const errorData = await response.json();
        alert(`Failed to add ${productName} to cart: ${errorData.message || response.statusText}`);
        console.error('Failed to add product to cart:', errorData);
      }
    } catch (error) {
      console.error('Network error or unexpected issue:', error);
      alert(`An error occurred while adding ${productName} to cart.`);
    }
  };

  return (
    <button onClick={handleAddToCart} className="add-to-cart-button">
      Add to Cart
    </button>
  );
};

export default AddToCartButton;

Explanation:

  • csrfToken: This is a placeholder. In a real application, your backend would embed this token in the HTML (e.g., <meta name="csrf-token" content="your-token">) or provide it via a dedicated API endpoint that the frontend fetches on initial load.
  • fetch('/api/cart/add', ...):
    • method: 'POST': This is a state-changing operation, ideal for CSRF protection.
    • headers: { 'X-CSRF-Token': csrfToken }: We explicitly add the CSRF token to a custom HTTP header. The backend will verify this token against the one it generated. If an attacker tries to forge this request from another site, they cannot read our csrfToken and thus cannot include this custom header.
    • credentials: 'include': This ensures that any HttpOnly cookies (like our authentication session cookie) are sent with the request.

Now, let’s add this button to each product card in ProductList.

Update src/components/ProductList.tsx:

// src/components/ProductList.tsx
import React, { useState, useEffect } from 'react';
import DOMPurify from 'dompurify';
import AddToCartButton from './AddToCartButton'; // Import AddToCartButton

// Define product interface
interface Product {
  id: string;
  name: string;
  description: string;
  price: number;
}

const ProductList: React.FC = () => {
  const [products, setProducts] = useState<Product[]>([]);
  const [loading, setLoading] = useState<boolean>(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const fetchProducts = async () => {
      try {
        const response = await new Promise<Product[]>(resolve =>
          setTimeout(() => resolve([
            { id: '1', name: 'Secure Laptop', description: 'A laptop with advanced security features. <script>alert("XSS attempt!");</script>', price: 1200 },
            { id: '2', name: 'Encrypted USB Drive', description: 'Keeps your data safe. <b>Strong encryption!</b>', price: 50 },
            { id: '3', name: 'Privacy Screen Filter', description: 'Protects your screen from prying eyes. Enjoy peace of mind.', price: 30 },
          ]), 500)
        );
        setProducts(response);
      } catch (err) {
        console.error('Failed to fetch products:', err);
        setError('Failed to load products. Please try again later.');
      } finally {
        setLoading(false);
      }
    };

    fetchProducts();
  }, []);

  if (loading) return <p>Loading products...</p>;
  if (error) return <p className="error-message">{error}</p>;

  return (
    <div className="product-list-container">
      <h3>Our Products</h3>
      <div className="products-grid">
        {products.map(product => (
          <div key={product.id} className="product-card">
            <h4>{product.name}</h4>
            <p>Price: ${product.price.toFixed(2)}</p>
            <p>Description:</p>
            <div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(product.description) }} />
            <AddToCartButton productId={product.id} productName={product.name} /> {/* Add the button */}
          </div>
        ))}
      </div>
    </div>
  );
};

export default ProductList;

Now, log in and click the “Add to Cart” buttons. You’ll see the simulated API call in the console and an alert confirming the action. This demonstrates how to include a CSRF token in your API requests.

Mini-Challenge: Secure User Review Display

Imagine your e-commerce site allows users to write reviews for products. These reviews are user-generated content and are prime targets for XSS.

Challenge: Modify the ProductList component to include a ProductReview component that displays a single review.

  1. Add a reviews: string[] property to the Product interface.
  2. Add some dummy reviews (some safe, some with XSS attempts) to your product data in ProductList.
  3. Create a new component src/components/ProductReview.tsx that takes a reviewText: string as a prop.
  4. Inside ProductReview.tsx, display the reviewText using dangerouslySetInnerHTML.
  5. Crucially, ensure that the reviewText is sanitized using DOMPurify before it is passed to dangerouslySetInnerHTML.
  6. Render ProductReview for each review in your ProductList.

Hint: Remember to use DOMPurify.sanitize() right before you assign the HTML string to __html.

What to observe/learn: You should see the reviews displayed correctly, but any malicious scripts within the dummy reviews should be neutralized, preventing them from executing. This reinforces the importance of sanitization for all untrusted content.

Common Pitfalls & Troubleshooting

  1. Over-reliance on Client-Side Validation:

    • Pitfall: Thinking that because you’ve added required attributes or JavaScript validation to your forms, your application is secure. Attackers can easily bypass frontend validation.
    • Troubleshooting: Always remember that client-side validation is for user experience and immediate feedback. Your backend must implement robust validation and sanitization for all incoming data. If you’re seeing unexpected data or vulnerabilities despite frontend validation, check your backend’s input handling.
  2. Storing Authentication Tokens in localStorage:

    • Pitfall: It’s convenient to store JWTs or session IDs in localStorage because JavaScript can easily access them. However, this makes them vulnerable to XSS attacks. If an attacker injects a script, they can read localStorage and steal the token, gaining access to the user’s account.
    • Troubleshooting: Prefer HttpOnly and Secure cookies for authentication tokens. The browser manages these cookies, preventing JavaScript access (HttpOnly) and ensuring transmission only over HTTPS (Secure). If you must use localStorage for some reason (e.g., refresh tokens in very specific scenarios), ensure extreme XSS protection measures are in place, and understand the inherent risks. For this project, we simulated the HttpOnly cookie approach.
  3. Using dangerouslySetInnerHTML Without Sanitization:

    • Pitfall: Directly inserting unsanitized user-generated content (like product reviews or descriptions) into dangerouslySetInnerHTML is a direct route to XSS.
    • Troubleshooting: Always, always, always sanitize any untrusted HTML content using a library like DOMPurify before passing it to dangerouslySetInnerHTML. If you’re seeing unexpected script execution or content distortion, check where you’re rendering HTML and ensure proper sanitization. If you don’t need to render actual HTML tags (e.g., just plain text), simply rendering the variable {myContent} in JSX is safest, as React automatically escapes it.

Summary

Phew! You’ve just built a basic React e-commerce frontend with several key security considerations baked in. Here’s a quick recap of what we covered:

  • Secure Authentication Flow: We implemented a simulated authentication process using an AuthProvider and AuthContext, emphasizing the importance of HttpOnly, Secure, and SameSite cookies (managed by the backend) for token storage.
  • XSS Prevention in React: We learned that React automatically escapes content rendered in JSX, providing strong default XSS protection. For scenarios requiring raw HTML (like rich text), we demonstrated how to use DOMPurify to sanitize content before using dangerouslySetInnerHTML.
  • CSRF Protection: We saw how to include a CSRF token in state-changing API requests (like adding to cart) via custom HTTP headers, complementing SameSite cookie policies.
  • Secure API Communication: We discussed the necessity of HTTPS, client-side validation for UX (not security), and careful error handling to avoid information leakage.
  • Secure State and Storage: We reiterated the dangers of storing sensitive authentication tokens in localStorage and advocated for HttpOnly cookies.

Building secure applications is an ongoing process, not a one-time task. This hands-on project has given you a practical foundation for applying security principles in a modern frontend framework. Keep practicing these habits, and always question the source of your data!

What’s Next?

In the next chapter, we’ll continue our journey by looking at framework-specific security considerations for Angular, another popular frontend framework. You’ll see similarities and differences in how Angular handles security challenges compared to React.

References


This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.