STATUS: FIXED IN PRODUCTION. The event.origin check is present in the current springboard-ui build (anaplan:version 1.0.37391, assets/index-BByDTUW_.js). This page is preserved as the original proof of concept from 13 July 2026. Running it against production today will not produce a toast — that is the fix working, not a broken PoC.
▶ Working proof of concept, despite the fix: /replay — runs the actual shipped listener code from both builds, verbatim, in a harness on a different origin, driven by a real cross-origin postMessage. Left frame (13 July listener) executes the attacker's tool calls; right frame (today's listener) rejects the identical payload. The fix is the only variable.
Where to look (for triage): if you run the button below against the current build, the attacker messages still arrive at the victim window — they are simply no longer acted on. Delivery vs. execution is the whole question, so both were measured separately; see “Measured on the current build” below. To confirm the handler itself is still alive, use the same-origin control at the bottom.

What this page does

This page is served from origin — an origin that is not id1.app.anaplan.com. It opens a victim's Springboard board designer (top-level navigation, so the victim's SameSite=Lax Anaplan session cookies flow and the board loads authenticated), then sends cross-window postMessage tool calls to it. On the build that was live when this was reported, the designer's message listener performed no event.origin check, so those calls executed in the victim's authenticated session — a toast reading CROSS-ORIGIN-CSRF-PROOF appeared on the victim's board.

The code, before and after

Pre-fix — assets/index-DFriS-5T.js (the build named in the report; now HTTP 404)

vy = async t => {
   if (!t.data || !t.data.tool_call_id || t.data.type === Dye) return;   // no origin comparison anywhere
   const s = t.data.name;
   ...
   const r = e.handlers[s];        // attacker-named handler
   if (r) { const a = await r(o); ... }   // invoked with attacker data
 };
 window.addEventListener("message", vy);

Preserved verbatim in an archived copy of the entry bundle downloaded 2026-07-10 00:31 UTC+2 (sha256 d5a86582f56abb5fc1b31e8938e43e4316e40d13aceb124b1e510b0e3837a60e, 4 886 072 bytes), three days before the report was filed.

Current — assets/index-BByDTUW_.js

Ny = async t => {
   if (t.origin !== window.location.origin || !t.data || !t.data.tool_call_id || t.data.type === cSe) return;
   ...
 };
 window.addEventListener("message", Ny);

Verifiable unauthenticated, no test tenant required:

curl -s https://id1.app.anaplan.com/a/springboard-ui/ | grep -o 'assets/index-[^"]*\.js'
curl -s https://id1.app.anaplan.com/a/springboard-ui/assets/index-BByDTUW_.js \
  | grep -o 'Ny=async t=>{if(t.origin!==window.location.origin[^;]\{0,80\}'
curl -sI https://id1.app.anaplan.com/a/springboard-ui/assets/index-DFriS-5T.js | head -1    # 404

Measured on the current build (2026-07-30)

Sender originMessages delivered to the victim windowHandlers executed
this page (cross-origin)183 — delivery works fine0 — rejected at the origin gate
https://id1.app.anaplan.com (same-origin control)11 — toast rendered

Delivery was counted with a capture-phase message listener installed in the victim page before application code ran, so it counts what reached the window regardless of what the app did with it. 183 attacker messages arrived and none were acted on — on the pre-fix build the same traffic produced the toast.

Expected on the pre-fix build: a CROSS-ORIGIN-CSRF-PROOF toast on the victim's board within a few seconds of it finishing loading. Expected on the current build: nothing. The messages are sent every 500 ms for ~30 s because the designer needs 5–15 s to mount and register the listener.

Log



 

Same-origin control (works on any build)

In the victim's Anaplan designer tab, open DevTools and paste this. It posts to the top window and to every same-origin iframe, and prints a diagnostic line so a silent no-op tells you why:

(() => {
  const msg = { tool_call_id:"c1", name:"show_message", data:{ message:"SAME-ORIGIN-CONTROL" } };
  const frames = [...document.querySelectorAll('iframe')]
    .map(f => { try { return f.contentWindow } catch(e) { return null } }).filter(Boolean);
  [window, ...frames].forEach(w => { try { w.postMessage(msg, location.origin) } catch(e) {} });
  console.log('[control] origin:', location.origin,
              '| designer mounted in this frame:', /Board designer|Exit designer/.test(document.body.innerText),
              '| posted to', 1 + frames.length, 'window(s)');
})()

The toast renders. That proves the front_end_tool bridge and the show_message handler are still present and still execute — the only thing that changed is that foreign origins are now rejected. On the pre-fix build, a message with a foreign event.origin was processed identically to this one; that was the bug.

If no toast appears, check these three first — the listener genuinely isn't there yet:

SymptomCause
diagnostic prints designer mounted in this frame: falseThe board is in view mode, or the designer hasn't finished mounting. The listener is registered only by the designer — the header must read “Board designer” with Preview / Exit designer / Save / Publish.
the tab shows the Anaplan login, or the board silently redirectsSession expired. The att cookie lives ~35 minutes; log in again and reopen the /edit URL.
diagnostic prints posted to 1 window(s) on a page that embeds the designerWrong frame. Open the standalone designer URL /a/springboard-ui/app/{app}/boards/{page}/edit directly, or pick the springboard-ui frame in the DevTools context dropdown.

Minimal PoC, verbatim from the report

The unmodified attacker page as submitted, on its own at /attacker.html — one button, no framing, nothing else.

This page collects and stores nothing. It only calls window.open() and postMessage() against the URL above.