How to Secure useParams Hook in React

In React applications, the useParams hook from React Router is widely used to retrieve dynamic parameters from the URL. However, because URL parameters are user-controlled inputs, they present significant security and stability risks, such as Cross-Site Scripting (XSS), SQL injection via backend API calls, and runtime application crashes. This article demonstrates how to secure the useParams hook by implementing strict type safety, schema validation, and sanitization.

The Risks of Unsecured URL Parameters

The useParams hook returns an object of key/value pairs of URL parameters, where every value is implicitly a string or undefined. Relying on these values without validation introduces several vulnerabilities:

To mitigate these risks, follow these three steps to secure your route parameters.


Step 1: Use Schema Validation with Zod

The most robust way to secure useParams is by validating the parameters against a schema as soon as they are retrieved. Using a validation library like Zod ensures that the data matches the expected type and format before your component uses it.

Here is how to implement schema validation:

import { useParams, Navigate } from 'react-router-dom';
import { z } from 'zod';

// Define a strict schema for your URL parameters
const ProductParamsSchema = z.object({
  productId: z
    .string()
    .regex(/^\d+$/, "Product ID must be a numeric string")
    .transform((val) => parseInt(val, 10)),
});

export function ProductDetails() {
  const params = useParams();

  // Safely parse the parameters
  const validation = ProductParamsSchema.safeParse(params);

  // If validation fails, handle the error safely (e.g., redirect to a 404 page)
  if (!validation.success) {
    return <Navigate to="/404" replace />;
  }

  // Retrieve the safely typed and parsed parameter
  const { productId } = validation.data;

  return (
    <div>
      <h1>Product Details</h1>
      <p>Viewing product ID: {productId}</p>
    </div>
  );
}

By using safeParse, the application avoids throwing runtime errors and allows you to gracefully redirect users or display a clean error state if the URL is tampered with.


Step 2: Sanitize Input Before API Calls

If you pass URL parameters directly to a backend API, they must be sanitized to prevent injection attacks. Even if your backend has its own validation, sanitizing on the frontend acts as a vital layer of defense-in-depth.

Avoid direct string interpolation in API requests:

// BAD: Vulnerable to path traversal and injection
fetch(`/api/products/${params.id}`); 

Instead, ensure the parameter matches your expected pattern and use URL encoding if the parameter contains special characters:

// GOOD: Validated and safely encoded
const safeId = encodeURIComponent(String(productId));
fetch(`/api/products/${safeId}`);

Step 3: Implement Strict TypeScript Types

If you are using TypeScript, do not cast useParams using as. Forcing a type with as MyParams bypasses TypeScript’s safety guarantees because the runtime URL value can still be anything.

Instead, use TypeScript to enforce optionality and handle the undefined state explicitly:

import { useParams } from 'react-router-dom';

interface RouteParams {
  userId?: string;
}

export function UserProfile() {
  // TypeScript enforces that userId might be undefined
  const { userId } = useParams<keyof RouteParams>() as RouteParams;

  if (!userId) {
    return <p>Error: User ID is required.</p>;
  }

  return <p>User ID: {userId}</p>;
}

Step 4: Authorize the Resolved Parameter

Security does not stop at validation. Once you have verified that the parameter is of the correct type (e.g., an integer ID), you must authorize the user’s access to that specific resource.

Always verify on the client (and enforce on the server) that the authenticated user has permission to access the resource represented by the validated parameter:

useEffect(() => {
  async function fetchUserData() {
    const response = await fetch(`/api/users/${userId}`, {
      headers: { Authorization: `Bearer ${userToken}` }
    });
    
    if (response.status === 403) {
      // Handle unauthorized access attempt
      triggerSecurityLog();
      showAccessDenied();
    }
  }
  fetchUserData();
}, [userId, userToken]);