From 9bffcf81b521124754cc0ea82225058ae8ac853b Mon Sep 17 00:00:00 2001 From: Chad Bentz <1760475+felickz@users.noreply.github.com> Date: Mon, 22 Jun 2026 21:21:15 -0400 Subject: [PATCH 1/2] JavaScript: Recognize Angular @HostListener('window:message') as a postMessage handler Angular registers window message handlers via the @HostListener('window:message', ['\']) decorator rather than window.addEventListener('message', ...). The PostMessageEventHandler class only modeled the addEventListener and window.onmessage forms, so the decorated handler's event parameter was never treated as a message source. As a result, js/missing-origin-check produced no alert and the event was not a client-side remote flow source for downstream queries (e.g. client-side URL redirection). Extend PostMessageEventHandler to also recognize methods decorated with @HostListener for 'window:message', 'document:message', or 'message'. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...-06-22-angular-hostlistener-postmessage.md | 4 +++ .../javascript/security/dataflow/DOM.qll | 12 ++++++++ .../CWE-020/MissingOriginCheck/Angular.ts | 29 +++++++++++++++++++ .../MissingOriginCheck.expected | 2 ++ 4 files changed, 47 insertions(+) create mode 100644 javascript/ql/lib/change-notes/2026-06-22-angular-hostlistener-postmessage.md create mode 100644 javascript/ql/test/query-tests/Security/CWE-020/MissingOriginCheck/Angular.ts diff --git a/javascript/ql/lib/change-notes/2026-06-22-angular-hostlistener-postmessage.md b/javascript/ql/lib/change-notes/2026-06-22-angular-hostlistener-postmessage.md new file mode 100644 index 000000000000..686e8ae96da5 --- /dev/null +++ b/javascript/ql/lib/change-notes/2026-06-22-angular-hostlistener-postmessage.md @@ -0,0 +1,4 @@ +--- +category: minorAnalysis +--- +* Added support for Angular's `@HostListener('window:message', ...)` and `@HostListener('document:message', ...)` decorators as `postMessage` event handlers. The decorated method's event parameter is now recognized as a client-side remote flow source, and is considered by the `js/missing-origin-check` query. diff --git a/javascript/ql/lib/semmle/javascript/security/dataflow/DOM.qll b/javascript/ql/lib/semmle/javascript/security/dataflow/DOM.qll index f959de6c0b5e..98beb1141c3f 100644 --- a/javascript/ql/lib/semmle/javascript/security/dataflow/DOM.qll +++ b/javascript/ql/lib/semmle/javascript/security/dataflow/DOM.qll @@ -195,6 +195,18 @@ class PostMessageEventHandler extends Function { rhs = DataFlow::globalObjectRef().getAPropertyWrite("onmessage").getRhs() and rhs.getABoundFunctionValue(paramIndex).getFunction() = this ) + or + // Angular's `@HostListener('window:message', ['$event'])` decorator registers + // a method as a `message` event handler on the global `window`/`document` + // target. The decorated method receives the `MessageEvent` as its first + // parameter, so it is equivalent to `window.addEventListener('message', ...)`. + exists(MethodDefinition method, DataFlow::CallNode decorator | + decorator = DataFlow::moduleMember("@angular/core", "HostListener").getACall() and + decorator = method.getADecorator().getExpression().flow() and + decorator.getArgument(0).mayHaveStringValue(["window:message", "document:message", "message"]) and + method.getBody() = this and + paramIndex = 0 + ) } /** diff --git a/javascript/ql/test/query-tests/Security/CWE-020/MissingOriginCheck/Angular.ts b/javascript/ql/test/query-tests/Security/CWE-020/MissingOriginCheck/Angular.ts new file mode 100644 index 000000000000..3a6695a0f65e --- /dev/null +++ b/javascript/ql/test/query-tests/Security/CWE-020/MissingOriginCheck/Angular.ts @@ -0,0 +1,29 @@ +import { Component, HostListener } from '@angular/core'; + +@Component({ selector: 'app-root' }) +class AngularComponent { + // Angular registers this as a `window` message handler via the decorator, + // equivalent to `window.addEventListener('message', ...)`. + @HostListener('window:message', ['$event']) + onWindowMessage(event: MessageEvent): void { // $ Alert - no origin check + eval(event.data); + } + + @HostListener('document:message', ['$event']) + onDocumentMessage(event: MessageEvent): void { // $ Alert - no origin check + eval(event.data); + } + + @HostListener('window:message', ['$event']) + onCheckedMessage(event: MessageEvent): void { // OK - has an origin check + if (event.origin === 'https://www.example.com') { + eval(event.data); + } + } + + // Not a message event, so it is not a postMessage handler. + @HostListener('window:resize', ['$event']) + onResize(event: MessageEvent): void { // OK - not a message handler + eval(event.data); + } +} diff --git a/javascript/ql/test/query-tests/Security/CWE-020/MissingOriginCheck/MissingOriginCheck.expected b/javascript/ql/test/query-tests/Security/CWE-020/MissingOriginCheck/MissingOriginCheck.expected index 58fb6ce79978..718826f82250 100644 --- a/javascript/ql/test/query-tests/Security/CWE-020/MissingOriginCheck/MissingOriginCheck.expected +++ b/javascript/ql/test/query-tests/Security/CWE-020/MissingOriginCheck/MissingOriginCheck.expected @@ -1,3 +1,5 @@ +| Angular.ts:8:19:8:23 | event | Postmessage handler has no origin check. | +| Angular.ts:13:21:13:25 | event | Postmessage handler has no origin check. | | tst.js:11:20:11:24 | event | Postmessage handler has no origin check. | | tst.js:24:27:24:27 | e | Postmessage handler has no origin check. | | tst.js:40:27:40:27 | e | Postmessage handler has no origin check. | From d1d9df772918347c46b39e2e39d73c73c192447e Mon Sep 17 00:00:00 2001 From: Chad Bentz <1760475+felickz@users.noreply.github.com> Date: Mon, 22 Jun 2026 21:35:21 -0400 Subject: [PATCH 2/2] Address review: restrict @HostListener handler to window/document message targets Drop the plain 'message' event name from the @HostListener matcher. The postMessage 'message' event is dispatched on window and does not bubble, so an element-level @HostListener('message') does not receive cross-window messages. Keeping only 'window:message' and 'document:message' makes the model more precise and matches the accompanying comment and change note. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- javascript/ql/lib/semmle/javascript/security/dataflow/DOM.qll | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/javascript/ql/lib/semmle/javascript/security/dataflow/DOM.qll b/javascript/ql/lib/semmle/javascript/security/dataflow/DOM.qll index 98beb1141c3f..3d371c473185 100644 --- a/javascript/ql/lib/semmle/javascript/security/dataflow/DOM.qll +++ b/javascript/ql/lib/semmle/javascript/security/dataflow/DOM.qll @@ -197,13 +197,13 @@ class PostMessageEventHandler extends Function { ) or // Angular's `@HostListener('window:message', ['$event'])` decorator registers - // a method as a `message` event handler on the global `window`/`document` + // a method as a `message` event handler on the global `window` or `document` // target. The decorated method receives the `MessageEvent` as its first // parameter, so it is equivalent to `window.addEventListener('message', ...)`. exists(MethodDefinition method, DataFlow::CallNode decorator | decorator = DataFlow::moduleMember("@angular/core", "HostListener").getACall() and decorator = method.getADecorator().getExpression().flow() and - decorator.getArgument(0).mayHaveStringValue(["window:message", "document:message", "message"]) and + decorator.getArgument(0).mayHaveStringValue(["window:message", "document:message"]) and method.getBody() = this and paramIndex = 0 )