Description
Client-side request forgery can arise when browser code builds URLs for fetch, XHR, or axios directly from user input. An attacker may manipulate query strings, hashes, or form fields to send requests to unintended paths, such as through ../, or to unapproved hosts. If the target API accepts a state-changing request, it may have an effect even when the response cannot be read. Rendering a response through innerHTML can also cause XSS. Whether a request succeeds and what it affects depend on browser security policies, credential options, and the target API's authentication and authorization.
Potential impact
- Path manipulation or arbitrary host selection may direct requests to internal APIs or administrative endpoints.
- A signed-in user's browser may be induced to make unintended API calls that read or change data.
- Rendering an untrusted response with
innerHTMLmay execute scripts or alter content. - The browser may be abused to send unwanted requests to local or corporate network resources, such as
127.0.0.1or internal hosts.
Remediation
- Fix the URL host or select it only from a predefined allow-list.
- Validate the entire input format and permitted range. Converting with
NumberorparseIntis not complete validation. Prevent path manipulation with inputs such as../,//, or%2e%2e. - Use a fixed base URL and assemble only validated path segments.
URLandencodeURIComponentalone do not ensure an authorized path; also check the final origin and path. - Render responses using
textContentrather thaninnerHTML, or use a trusted sanitizer when HTML is necessary. - Limit fetch options such as
credentialsandmethodto what is needed, and apply server-side authentication, authorization, and CSRF defenses. CORS alone cannot prevent request transmission or state changes.
Examples
Before
javascript
async function showProfile() {
const qs = new URLSearchParams(location.search);
const next = qs.get("next"); // Example: https://evil.example/steal
const userId = qs.get("id"); // Example: ../../admin/metrics
// BAD 1: use an entire untrusted URL
const r1 = await fetch(next);
// BAD 2: concatenate input into the path (possible path traversal)
const r2 = await fetch("/api/user/" + userId);
const data = await r2.json();
// BAD 3: insert an unvalidated response into innerHTML, risking XSS
document.getElementById("content").innerHTML = data.html;
}
After
javascript
async function showProfileSafe() {
// 1) Use only an approved host
const BASE = "https://api.example.com";
// 2) Constrain path parameters to a schema (numbers) or validate strictly
const qs = new URLSearchParams(location.search);
const rawId = qs.get("id");
const id = Number(rawId);
if (!Number.isInteger(id) || id < 0) {
throw new Error("invalid id");
}
// 3) Keep the base fixed and assemble only validated segments
const url = new URL(`/v1/user/${id}`, BASE);
const res = await fetch(url.toString(), {
method: "GET",
credentials: "same-origin",
});
const data = await res.json();
// 4) Display content with textContent instead of innerHTML
const box = document.getElementById("content");
box.textContent = data.name; // Use a trusted sanitizer if HTML is needed
}
Explanation:
- Before:
- Input (
next,id) becomes the fetch URL directly, permitting arbitrary hosts or path traversal. - Inserting the response into
innerHTMLcan cause XSS.
- Input (
- After:
- A fixed host and nonnegative integer path parameter prevent manipulations such as
../. The server must still validate the permitted ID range and access permissions. URL(base)constructs the URL without allowing input to replace the entire URL.textContentdisplays text without executing it as HTML.
- A fixed host and nonnegative integer path parameter prevent manipulations such as