CRZ-015

Widget shim postMessage origin bypass via user-controlled appName

medium CVSS 3.1: 4.3 · Asset: widget.simulator.company/shim.js

Summary

The shim script at widget.simulator.company/shim.js — loaded inside admin.corezoid.com per its CSP — registers a message event listener with a weak origin check that can be bypassed by user-controlled data:

addEventListener("message", function(r) {
  if (!!r && !!r.data && !!r.data.type) {
    var a = r.origin === fe.origin,      // proper origin check
        i = r.data.appName === s,        // bypass: attacker controls data.appName
        n = a || i,                      // ← OR means EITHER sufficient
        o = Boolean(e[r.data.type] || t[r.data.type]);
    if (n && o) {
      var l = r.data, u = l.namespace, d = l.actorId,
          f = l.type, c = l.payload, h = void 0===c ? {} : c,
          g = h.isLauncher;
      _e(u, d, f, h, void 0 !== g && g);  // invoke handler with attacker-controlled data
    }
  }
}, !1);

The i = r.data.appName === s check compares the appName field of the incoming message against a local variable s. Because r.data.appName comes from the message payload itself, an attacker can set it to match s — effectively passing the check without the real origin matching.

The logical flaw is n = a || i: if EITHER origin check OR appName check passes, the message is processed. A proper implementation would be n = a && i (both must hold) or — more correctly — n = a (only trust by origin).

Attack path

  1. Victim (Corezoid admin user) is logged into admin.corezoid.com.
  2. Victim opens an attacker-controlled page in another tab, or admin is embedded in an attacker-controlled iframe (mitigated if admin sets X-Frame-Options: DENY — it sets SAMEORIGIN, so a cross-origin iframe cannot frame admin).
  3. Attacker's page opens admin.corezoid.com in a popup window or iframe (if possible), OR uses window.postMessage on a reference it obtains.
  4. Attacker sends postMessage({appName: "<guessed-or-leaked-s-value>", type: "<known-type>", namespace: "...", actorId: "...", payload: {...}}, "*") at the admin-context shim.
  5. The shim processes the message and invokes _e(namespace, actorId, type, payload) — the behavior of _e determines impact.

Unknowns requiring follow-up

To establish exact severity:

  1. What is the s value? — the expected appName. Needs either:
    • Dynamic analysis (debug the shim in a live browser session, read the variable at runtime)
    • Deeper static analysis (trace through ~1.87MB of minified code to find where s is assigned — the minifier may have renamed it)
    • Or the iframe/parent that legitimately sends these messages may have the string visible in its own code
  2. What does _e(namespace, actorId, type, payload) do? — does it:
    • Just render UI (low impact)?
    • Call admin.corezoid.com APIs with attacker payload (authenticated-action CSRF bypass)?
    • Modify the DOM (potential DOM XSS)?
    • Invoke eval/Function (direct JS execution)?

I did not complete these items to stay within the conservative-scan RoE (running instrumented browser against production + longer bundle reverse would extend the engagement; I flagged the finding and stopped).

Evidence

Impact

If the downstream _e(...) handler performs any admin-privileged action or renders attacker-controlled content, this becomes effective cross-origin access to an authenticated admin session — bypassing SOP without needing a real XSS.

Common payloads for widgets accepting postMessage:

Remediation

Priority 1 — fix the origin check:

addEventListener("message", function(r) {
  // Strict origin check only — remove the appName bypass
  if (r.origin !== fe.origin) return;
  // ... proceed with safe handling
}, !1);

Or if multiple trusted origins need to be supported, maintain an allowlist of origins:

var ALLOWED = [fe.origin, "https://admin.corezoid.com", "https://sim.simulator.company"];
if (!ALLOWED.includes(r.origin)) return;

Never use message payload fields for trust decisions. The entire point of event.origin is that it is set by the browser and cannot be forged; using a payload field (appName) instead of the origin is substituting a strong authentication for a weak one.

Priority 2 — defense in depth:

  1. Drop any embedded-widget feature if it's not heavily used. The "editor_ai" widget is one admin UI enhancement; if its value is low, removing the integration simplifies the attack surface.
  2. Scope the shim to read-only operations only — never let postMessage trigger a state-changing API call.
  3. Audit _e handler (the downstream function) for injection sinks.
  4. Consider using the MessageChannel pattern instead of broadcast postMessage — this provides a dedicated port with implicit origin pinning.

References