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:
- Type Mismatches: Assuming a parameter is a number (like an ID) when a user has entered random text, leading to database query failures or frontend crashes.
- XSS Attacks: If the parameter is directly rendered
into the DOM using properties like
dangerouslySetInnerHTML, malicious scripts can be executed. - Insecure Direct Object References (IDOR): Accessing resources directly via URL IDs without verifying if the current user has permission to view them.
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]);