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
localStorageis generally discouraged due to its vulnerability to XSS attacks, which could allow an attacker to steal the token. A more secure approach often involves usingHttpOnlyandSecurecookies, 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:
Explanation of the Diagram:
- User Enters Credentials: The user provides their username and password through a login form in the React application.
- React App Sends Credentials: The React app sends these credentials to the backend’s
/loginendpoint, typically via aPOSTrequest. - Backend Verification and Token Generation: The backend verifies the credentials. If valid, it generates an authentication token (like a JWT or session ID).
- Backend Sends Token Securely: Instead of sending the token in the response body for
localStoragestorage, the backend sets it in anHttpOnly,Secure, andSameSitecookie.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.
- 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.
- Redirect to Protected Area: The user is redirected to a part of the application that requires authentication.
- User Requests Protected Resource: The user navigates to a protected page or performs an action requiring authentication.
- 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 theHttpOnlycookie with the request. - Backend Validates Token: The backend receives the request, extracts the token from the cookie, and validates it.
- Backend Returns Protected Data: If the token is valid, the backend processes the request and returns the requested data.
- 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 <script> 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. UseHttpOnlycookies for authentication tokens. For non-sensitive, user-specific preferences,localStorageis 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 usesnpmto execute the latestvitepackage, 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 usesuseAuth.useState: Manages theisAuthenticatedboolean anduserobject.loginfunction: This simulates a call to your backend. In a real scenario, a successful backend response would set anHttpOnly,Secure,SameSitecookie in the browser, and your frontend would simply update itsisAuthenticatedstate based on the presence of that session (e.g., by making a/meendpoint call). For our demo, we just check dummy credentials.logoutfunction: Simulates logging out. A real logout might also involve an API call to invalidate the session on the backend.useAuthhook: 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 anyerrormessages.useAuth(): Accesses theloginfunction from ourAuthContext.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
loginfunction fromAuthContext. - Sets an error message if login fails.
aria-labelandrole="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
AppContentcomponent withAuthProvider. This makesisAuthenticatedand other auth functions available throughout our app. AppContentnow conditionally rendersDashboardifisAuthenticatedis true, otherwise it shows theLogincomponent. 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:
ProductInterface: 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 inproduct.descriptionfor the “Secure Laptop”.DOMPurify: This is a crucial library for sanitizing HTML. Before usingdangerouslySetInnerHTML, we pass the potentially untrustedproduct.descriptionthroughDOMPurify.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
- Installation: You’ll need to install
dangerouslySetInnerHTML: We use this after sanitization. If we just rendered{product.description}in JSX, React would automatically escape the HTML entities (<becomes<, etc.), which is the safest default behavior. We usedangerouslySetInnerHTMLhere 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
queryis just a string. When it’s passed toonSearch, 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
handleSearchfunction 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 ourcsrfTokenand thus cannot include this custom header.credentials: 'include': This ensures that anyHttpOnlycookies (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.
- Add a
reviews: string[]property to theProductinterface. - Add some dummy reviews (some safe, some with XSS attempts) to your product data in
ProductList. - Create a new component
src/components/ProductReview.tsxthat takes areviewText: stringas a prop. - Inside
ProductReview.tsx, display thereviewTextusingdangerouslySetInnerHTML. - Crucially, ensure that the
reviewTextis sanitized usingDOMPurifybefore it is passed todangerouslySetInnerHTML. - Render
ProductReviewfor each review in yourProductList.
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
Over-reliance on Client-Side Validation:
- Pitfall: Thinking that because you’ve added
requiredattributes 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.
- Pitfall: Thinking that because you’ve added
Storing Authentication Tokens in
localStorage:- Pitfall: It’s convenient to store JWTs or session IDs in
localStoragebecause JavaScript can easily access them. However, this makes them vulnerable to XSS attacks. If an attacker injects a script, they can readlocalStorageand steal the token, gaining access to the user’s account. - Troubleshooting: Prefer
HttpOnlyandSecurecookies for authentication tokens. The browser manages these cookies, preventing JavaScript access (HttpOnly) and ensuring transmission only over HTTPS (Secure). If you must uselocalStoragefor 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 theHttpOnlycookie approach.
- Pitfall: It’s convenient to store JWTs or session IDs in
Using
dangerouslySetInnerHTMLWithout Sanitization:- Pitfall: Directly inserting unsanitized user-generated content (like product reviews or descriptions) into
dangerouslySetInnerHTMLis a direct route to XSS. - Troubleshooting: Always, always, always sanitize any untrusted HTML content using a library like
DOMPurifybefore passing it todangerouslySetInnerHTML. 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.
- Pitfall: Directly inserting unsanitized user-generated content (like product reviews or descriptions) into
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
AuthProviderandAuthContext, emphasizing the importance ofHttpOnly,Secure, andSameSitecookies (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
DOMPurifyto sanitize content before usingdangerouslySetInnerHTML. - CSRF Protection: We saw how to include a CSRF token in state-changing API requests (like adding to cart) via custom HTTP headers, complementing
SameSitecookie 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
localStorageand advocated forHttpOnlycookies.
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
- OWASP Top 10 (2021): The authoritative list of the most critical web application security risks.
- MDN Web Docs - HTTP Cookies
SameSiteattribute: Essential for understanding CSRF protection with cookies. - MDN Web Docs -
HttpOnlycookie attribute: Explains how to prevent client-side script access to cookies. - DOMPurify GitHub Repository: Official documentation for the DOMPurify library.
- React Documentation -
dangerouslySetInnerHTML: Official guide on using this potentially risky prop. - Vite Documentation: For up-to-date setup and usage of Vite.
This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.