By Garrett Kohlrusch | GK Data LLC
Intigriti Challenge 0526 was designed around a client-side XSS problem. My solution reached a different persistent XSS path: a profile display name passed through a bypassable “SCA Shield” and later entered an unsafe innerHTML sink in the testimonials feed.
Intigriti’s official challenge recap describes the intended DOM-clobbering route and notes that researchers also found unintended bugs. The route below was confirmed as unintended in my challenge submission. Because this was a public security challenge, the proof payload is included for technical study.
The finding in one sentence
The testimonial body was passed through DOMPurify, but the author’s profile name was assigned directly to innerHTML. The input filter was therefore the only barrier protecting an executable browser sink.

Reading the renderer changed the investigation
I initially tested the testimonial content field. Reading the client-side render loop showed why that path was less interesting:
let textDiv = document.createElement('div');
textDiv.className = 'user-text';
textDiv.innerHTML = DOMPurify.sanitize(t.content);
The sibling display-name path received different treatment:
let nameDiv = document.createElement('div');
nameDiv.className = 'user-name';
nameDiv.innerHTML = t.user_name;
The content field was passed through a sanitizer. The profile name was treated as markup without an equivalent control. Once a user saved a malicious display name and posted any testimonial, visitors loading the feed rendered that name through the unsafe sink.
Observed SCA Shield behavior
The application returned different messages for forbidden punctuation and payload signatures. That behavior was consistent with separate character and signature checks, although the server-side filter implementation was not available to inspect.
Testing showed that quotes, parentheses, periods, commas, and semicolons were rejected. Common XSS substrings also triggered the signature response. Backticks, brackets, string concatenation, mixed-case HTML, and slash-separated attributes remained available. These observations were enough to design around the filter; they should not be read as an exhaustive map of its implementation.
From blocked input to executable markup
A basic image error-handler payload failed immediately. Replacing parentheses with tagged-template syntax removed forbidden characters, but recognizable tag, event, and function names still matched signatures. Mixed case and string fragmentation addressed those checks.
Passing the filter was only half the work. Several event patterns did not execute in the tested challenge and browser context after insertion through innerHTML. An autofocus input did:
<InPuT/AuToFoCuS/OnFoCuS=self[`al`+`ert`]`gkdata`>
The proof used mixed-case names and slash separators to avoid literal signatures. self[`al`+`ert`] reconstructed the function name at runtime. The final backtick expression invoked it as a tagged template; it is not generally equivalent to a normal function call, but it supplied a sufficient proof value in this context. In the required Chrome environment, autofocus caused the injected element to receive focus and the handler executed when the feed rendered.

Reproduction chain
- Create an account and open the profile route.
- Save the proof payload as the display name. The observed shield checks accept it.
- Post a testimonial so the display name appears in the feed.
- Load the testimonials route in the challenge’s required browser environment.
- The client assigns the stored display name to
innerHTML, creates the autofocus element, and executes the focus handler.
Root cause and remediation
The unsafe innerHTML assignment was the primary enabling condition. The signature-based blocklist was a fragile mitigation that could be bypassed without fixing the output context.
nameDiv.textContent = t.user_name;
A display name does not need HTML semantics, so textContent is the direct fix. The application should also audit every other innerHTML assignment, test stored values across all routes that consume them, and treat input filtering only as defense in depth.
What this challenge reinforced
- Read source before optimizing payloads. The important difference was one line between sibling render paths.
- Separate filter acceptance from browser execution. A value can pass server-side checks and still fail to produce an executable DOM.
- Record browser and challenge conditions. Event behavior should be described for the tested context, not as a universal browser rule.
- Fix the sink. Encoding or text-only insertion at the point of use is stronger than trying to recognize every hostile string.
This source-to-sink analysis is the same kind of manual reasoning used in GK Data web application and API testing. AI-assisted reconnaissance can organize code paths and hypotheses, but a researcher still has to confirm the exact data flow, execution condition, and impact.
