November 3, 2026
“Exploiting Trust: DOM Clobbering to Same-Origin Script Execution in Intigriti’s Registry…
Author: Md. Fahim Shakil Chowdhury Target: Intigriti BRP Challenge 1026 challenge-brp.challenges.intigriti.io Vulnerability Class…

By Md. Fahim Shakil Chowdhury
7 min read
Author: Md. Fahim Shakil Chowdhury Target: Intigriti BRP Challenge 1026 challenge-brp.challenges.intigriti.io Vulnerability Class: DOM-based XSS via DOM Clobbering (CWE-79, CWE-1321) Severity: Medium
1. Executive Summary:
The target application, a fictional "National Missing Person Registry," contains a critical client-side vulnerability architecture that allows unauthenticated attackers to achieve arbitrary JavaScript execution within the application's origin.
Despite the presence of robust defensive layers, including server-side HTML encoding and a strict, nonce-based Content Security Policy (CSP), the application is vulnerable to an advanced DOM clobbering attack. By exploiting a double-parse flaw and bypassing a client-side blocklist sanitizer, an attacker can inject malicious DOM elements. These elements subsequently hijack the application's trusted script-loading gadget, tricking the application into fetching and executing a same-origin JavaScript payload. Furthermore, successful exploitation requires bypassing a hidden server-side raw-URL validation check, demonstrating a complex chain of trust boundary failures.
2. The "How This Hack Works" Breakdown:
Imagine a highly secure office building (the website) with a strict front desk guard (the server) and an interior security robot (the client script).
The Guard is Ignored: When you submit a search query, the front desk guard properly checks it, makes it safe and prints it on a visitor badge. However, the interior robot completely ignores this safe badge, walks outside and grabs your original, unchecked query directly from your hands (the URL).
The Blind Spot: The robot scans your query for dangerous items like weapons (<script>) and removes them. But the robot is programmed to ignore fake ID cards (HTML attributes like id and data-*). It lets your fake ID cards into the building.
Identity Theft (DOM Clobbering): The website has a trusted file-delivery system that relies on two specific ID cards (archive-latch & record-anchor) to decide which files to load. Because our fake ID cards slipped past the robot, we "clobbered" (overwrote) the real ones. When the website looks for its trusted cards, it finds ours instead.
Using the Rules Against Themselves: The website has an iron-clad rule called a Content Security Policy (CSP) that states, "I will only run code from my own building." We never broke this rule. Instead, our fake ID cards tricked the website into loading a file (/echo.php) that already exists inside the building, but we manipulated that file to execute our own JavaScript (alert()). The website executed our attack because it thought it was following its own strict rules.
Raw URL Input ➔ Client-Side Double Parse ➔ DOM Elements Clobbered ➔ Trusted Loader Hijacked ➔ Same-Origin Script ExecutedRaw URL Input ➔ Client-Side Double Parse ➔ DOM Elements Clobbered ➔ Trusted Loader Hijacked ➔ Same-Origin Script Executed3. Reconnaissance & Defensive Posture:
The application is designed to render a profile when searching for known names (sushil, mickey or ram). Searching for an unknown name triggers a client-side JavaScript block that dynamically displays a "not found" message. Initial reconnaissance reveals a highly restrictive defensive posture:
Server-Side Encoding: PHP is correctly used htmlspecialchars() to encode inputs before rendering the HTML to the browser. This neutralizes standard reflected XSS.
Client-Side Sanitization: An inline JavaScript blocklist actively removes potentially dangerous elements and attributes.
Strict CSP: The application enforces default-src 'none'; script-src 'nonce-…' 'self'; base-uri 'none'. There are no unsafe-inline permitted, entirely eliminating inline handlers and standard injection vectors.
4. Vulnerability Analysis: The Attack Chain
Phase 1: One Query Value, Two Readers
The root cause of this vulnerability is an architectural discrepancy defined as "ONE QUERY VALUE, TWO READERS".
The Safe Path (PHP): The server utilizes escape htmlspecialchars to escape the input. This effectively renders the input safe. However, PHP's escaped copy goes nowhere and is never used.
The Unsafe Path (JavaScript): The page's JavaScript reads the raw URL value again using new URLSearchParams(location.search).get('person')[cite: 5]. It parses the input, filters it, and parses it again.
Phase 2: Sanitize Once, Parse Twice (The Sanitizer Bypass)
The application attempts to sanitize the raw parameter using a temporary <template> element as a sandbox:
var holder = document.createElement('template');
holder.innerHTML = String(note); // FIRST PARSE
// Blocklist removes <script>, <iframe>, <object>, <embed>
Array.prototype.forEach.call(holder.content.querySelectorAll('script,iframe,object,embed'), function (node) { node.remove(); });
// Blocklist removes all on* attributes (e.g., onerror, onload)
Array.prototype.forEach.call(holder.content.querySelectorAll('*'), function (node) { ... });
// SECOND PARSE: Serialized back to a string and injected into the live DOM
profile.innerHTML = holder.innerHTML + '<p class="status"> not found</p>';var holder = document.createElement('template');
holder.innerHTML = String(note); // FIRST PARSE
// Blocklist removes <script>, <iframe>, <object>, <embed>
Array.prototype.forEach.call(holder.content.querySelectorAll('script,iframe,object,embed'), function (node) { node.remove(); });
// Blocklist removes all on* attributes (e.g., onerror, onload)
Array.prototype.forEach.call(holder.content.querySelectorAll('*'), function (node) { ... });
// SECOND PARSE: Serialized back to a string and injected into the live DOM
profile.innerHTML = holder.innerHTML + '<p class="status"> not found</p>';This blocklist approach is fundamentally flawed because it only targets conventionally executable elements. Any benign tag (like a <div> or <a>) carrying id and data-* attributes easily survives this filtering process. Because the "sanitized" string is subsequently assigned to profile.innerHTML, the browser parses it a second time, officially inserting the attacker's elements into the live DOM.
Phase 3: The Trusted Loader Gadget (The Clobber)
The double parse is the obvious failure, but the second failure matters more: the page trusted its own DOM as configuration. Following the injection, a trusted loader script executes:
marker = document.getElementById('record-anchor');
latch = document.getElementById('archive-latch');
// Guard logic: both elements must exist, and latch data-key must equal 'river-lantern'
if (!marker || !latch || latch.getAttribute('data-key') !== 'river-lantern') return;
var endpoint = marker.getAttribute('data-index');
if (endpoint !== '/echo.php?channel=echo&listener=alert(document.domain)//') return;
// The gadget dynamically creates a script tag using our clobbered data-index
var loader = document.createElement('script');
loader.src = endpoint;
document.head.appendChild(loader);marker = document.getElementById('record-anchor');
latch = document.getElementById('archive-latch');
// Guard logic: both elements must exist, and latch data-key must equal 'river-lantern'
if (!marker || !latch || latch.getAttribute('data-key') !== 'river-lantern') return;
var endpoint = marker.getAttribute('data-index');
if (endpoint !== '/echo.php?channel=echo&listener=alert(document.domain)//') return;
// The gadget dynamically creates a script tag using our clobbered data-index
var loader = document.createElement('script');
loader.src = endpoint;
document.head.appendChild(loader);Because the attacker controls the injected id attributes marker and latch points to the injected payload rather than legitimate application elements. The loader reads the two surviving injected elements. The loader uses these elements to build a <script src=/echo...> tag. This dynamic script injection is allowed by the script-src 'self' CSP directive. The CSP was strict and correct, but it just cannot tell the page's intent from the page's bugs.
Phase 4: Beating the "403 Wall" (Server-Side Validation)
Injecting the correct DOM elements is insufficient. If an attacker injects the correct DOM elements but fails a hidden server-side check, the network request /echo.php fires perfectly on the client, but the server responds with 403 Forbidden an empty body.
Controlled testing of payload variations reveals the strictness of this server-side check (which likely validates the raw Referer header):
Complete fragment, empty anchor text (<a></a>): 403 Forbidden.
Complete fragment, anchor text x: 403 Forbidden.
Complete fragment, anchor text, signal but no closing tags (</div>): 403 Forbidden. (Note: The browser normalizes and closes the tags in the DOM, making the client-side DOM look perfect, but the raw URL check on the server fails).
Complete fragment, anchor text signal properly closed tags: 200 OK.
The keyword signal is deliberately required; in Ram's valid profile, he is described as a radio keeper who answers calls even when the "signal" disappears. The server mandates this specific string and well-formed HTML tags to be present in the raw query string.
5. Exploitation & Proof of Concept:
Step-by-Step Reproduction:
Craft the HTML: Build the tags exactly as the trusted loader expects, utilizing the required "signal" text and ensuring all tags are closed to satisfy the server-side checks.
<div id="archive-latch" data-key="river-lantern"><a id="record-anchor" data-index="/echo.php?channel=echo&listener=alert(document.domain)//">signal</a></div>
URL Encode & Execute: Submit the URL-encoded payload as the person parameter.
The Weaponized URL:
https://challenge-brp.challenges.intigriti.io/challenge.php?person=%3Cdiv%20id%3D%22archive-latch%22%20data-key%3D%22river-lantern%22%3E%3Ca%20id%3D%22record-anchor%22%20data-index%3D%22%2Fecho.php%3Fchannel%3Decho%26listener%3Dalert(document.domain)%2F%2F%22%3Esignal%3C%2Fa%3E%3C%2Fdiv%3Ehttps://challenge-brp.challenges.intigriti.io/challenge.php?person=%3Cdiv%20id%3D%22archive-latch%22%20data-key%3D%22river-lantern%22%3E%3Ca%20id%3D%22record-anchor%22%20data-index%3D%22%2Fecho.php%3Fchannel%3Decho%26listener%3Dalert(document.domain)%2F%2F%22%3Esignal%3C%2Fa%3E%3C%2Fdiv%3E
Execution Quirks & Timing:
The execution flow requires precise timing due to custom application logic wrapping the native alert and backend rate limiting:
HTTP GET (/challenge.php): The DOM Clobbering markers successfully land in #profile, and the trusted loader appends the script request to the <head>.
HTTP GET (/echo.php): The same-origin script fetches successfully (HTTP 200).
Code Execution: The fetched script executes alert(document.domain).
Flag Retrieval: The application overrides window.alert. Upon dismissing the native alert dialog, the script automatically sends a same-origin POST request to /reveal.php. The flag (INTIGRITI{f2cb05a5-5cc1-4d1d-b7cd-084306cbd5a0}) is written to the #vault element.
Timing Note 1: The exploit worked twice in the same session, so you don't need a fresh one.
Timing Note 2: Once, with the alert left open for about a minute late in a long session, the reveal endpoint gave a 403.
Timing Note 3: A rapid reload got a 429 status code. The limit is three requests a second, and the page fires three on its own.
6. Analysis of Alternative Attack Vectors
During the analysis of the target, several alternative methodologies and bypass attempts were evaluated to confirm the exact boundaries of the CSP and server-side checks:
Direct onerror payloads, revived onerror payloads iframe srcdoc, SVG use andanimate, meta refresh, javascript: URLs, and <base> tags are all stopped by the sanitizer, the browser, or the CSP.
Calling /echo.php yourself, using any channel, header or method, stays a 403 without the right page behind it.
Calling /reveal.php yourself, with the challenge_person in-the-query body, cookie, or JSON, gets warnings or a 403.
Splitting the person parameter between PHP and JavaScript using person[] or duplicate keys either skips the client branch entirely or echo still says 403.
Hunting for source code files such as robots.txt, /lib/*, .git, .env, and backup suffixes yields nothing.
7. Vulnerability Impact
Within the context of the challenge, the script runs in the registry's origin from a link, requiring only one click and no other interaction. On a real site, that is the standard reflected DOM XSS outcome, meaning the attacker's code runs as the victim on that origin.
This allows an attacker to completely bypass CSRF protections, exfiltrate sensitive session tokens (if not protected by the HttpOnly flag), and pivot into account takeover. The strict CSP did not help to prevent this, because the page itself held the gadget that loaded the script.
8. Remediation Recommendations
To securely patch this architecture, the following mitigations should be implemented:
Decouple Configuration from the DOM (Highest Priority): Never let DOM content decide a script URL. If the loader needs a value, put it directly in the script or in a nonce-protected JSON block that the server writes. If only one of these fixes could be shipped, it should be this one. The double parse is what got the elements in, but the loader is what turned them into an executable script.
Single Source of Truth: Don't re-read the person parameter on the client. Render the not-found state on the server, where it is already escaped.
Safe Sinks: If the rendering has to be dynamic, put it in with textContent. A name is never markup.
Allowlist Sanitization: If you must sanitize, use one maintained sanitizer, and insert its output without parsing it a second time. Ensure the sanitizer strips structural attributes like id and data-* from untrusted input to prevent DOM Clobbering entirely.