CodeQL 2.26.1 expanded its Angular analysis so events received through @HostListener('window:message') and @HostListener('document:message') can be treated as remote input. It also added a query for message handlers that do not verify the sender’s origin.

That change can reveal a real trust-boundary mistake. postMessage is commonly used between a page and an iframe, an authentication popup, or an embedded payment flow. The receiving event can come from a different website unless the handler proves otherwise.

Quick answer

Check event.origin against an exact allowlist before reading event.data. Then validate the shape of the message and reject unknown action names. Do not use *, substring matching, or a suffix check as the security decision.

const allowedOrigins = new Set([
  "https://accounts.example.com",
  "https://checkout.example.com",
]);

@HostListener("window:message", ["$event"])
onMessage(event: MessageEvent<unknown>) {
  if (!allowedOrigins.has(event.origin)) return;
  if (!isTrustedMessage(event.data)) return;

  this.handleTrustedMessage(event.data);
}

An origin contains the scheme, hostname, and port. https://example.com and http://example.com are different origins.

Why the new alert matters

A message event is not trusted because it reached your Angular component. If an attacker can open the page, embed it, or cause a connected window to send data, the handler may process attacker-controlled values.

The dangerous outcome depends on what the handler does. It may update account state, copy an access token, navigate to a URL, start a payment step, or place HTML in the page. Even a harmless-looking message can become risky when it reaches another sink later.

CodeQL’s change helps connect the source to those later operations. It does not prove every result is exploitable, but it gives reviewers the correct place to start: who is allowed to send this message?

Review the origin check carefully

Exact comparison is easier to audit than clever string logic.

// Unsafe: evil-example.com also ends with example.com
if (event.origin.endsWith("example.com")) {
  processMessage(event.data);
}

If development and production require different senders, inject the allowlist from a reviewed environment configuration. Do not accept arbitrary origins merely because the app is running locally.

Also check the sender side. When calling postMessage, provide the expected target origin instead of "*".

paymentFrame.contentWindow?.postMessage(
  { type: "payment-ready", orderId },
  "https://checkout.example.com",
);

Validate the message after its origin

An approved origin can still send malformed data because of bugs, compromised content, or a confused integration. Treat the message as an API request with a small schema.

type TrustedMessage = {
  type: "payment-complete";
  orderId: string;
};

function isTrustedMessage(value: unknown): value is TrustedMessage {
  if (!value || typeof value !== "object") return false;
  const candidate = value as Record<string, unknown>;
  return candidate.type === "payment-complete"
    && typeof candidate.orderId === "string"
    && candidate.orderId.length <= 100;
}

Keep the allowed action names narrow. Avoid passing method names, routes, or HTML directly from the message into privileged code.

Test the trust boundary

Create a small test page on an unapproved origin and attempt to send the same payload as the trusted integration. The application should ignore it without changing state. Then test the approved origin with a malformed payload and confirm that it is also rejected.

In unit tests, cover:

  • exact approved origin and valid payload;
  • approved origin with an unknown message type;
  • unapproved origin with an otherwise valid payload;
  • lookalike subdomain and HTTP downgrade;
  • missing, oversized, and nested unexpected fields.

The test should assert the protected action did not run, not merely that no exception appeared.

Triage existing CodeQL findings

Start with handlers that touch authentication, payments, navigation, storage, or DOM rendering. Confirm whether each handler is reachable in production and list the actual sending pages.

If a framework wrapper already verifies the origin, document that data flow before dismissing the result. A suppression should point to a concrete guard and test. “This iframe is ours” is not evidence when the code does not enforce that assumption.

Primary sources

The useful response to the new alert is not to silence a noisy query. Establish an exact sender policy, validate the payload, test an untrusted sender, and record why every remaining exception is safe.

Re-run the query after the guard is merged and keep the hostile-origin test in CI.