CVE-2026-28861
Overview
Background
- UserMessageHandler
- The WebCore object backing window.webkit.messageHandlers.<name>, bridging page script to a host-registered script message handler for a specific frame.
- UserMessageHandlerDescriptor
- The registered named handler (added via the WebKit2 API) that a UserMessageHandler dispatches messages to.
- Same-origin policy
- The web security rule that content from one origin must not access resources or capabilities bound to a different origin.
- SecurityOrigin / isSameOriginAs
- WebCore’s representation of an origin (scheme, host, port) and the comparison used to enforce same-origin access decisions.
- scriptExecutionContext
- The context (document/worker) associated with a JSDOMGlobalObject, from which the caller’s actual security origin is obtained.
Root Cause Analysis
UserMessageHandler implements the WebKit script message handler bridge (window.webkit.messageHandlers.<name>.postMessage / postLegacySynchronousMessage) that lets page script deliver messages to a host-registered WKScriptMessageHandler. A UserMessageHandler is constructed for a specific LocalFrame and wraps a UserMessageHandlerDescriptor (the named handler registered via the WebKit2 API). Before the patch, postMessage() and postLegacySynchronousMessage() only checked that the descriptor still existed; they did not verify that the JavaScript caller’s security origin matched the origin of the frame the handler belongs to. The violated invariant is that a message handler registered for a given frame/origin must only be reachable by script from that same origin. Because a handler object obtained in one frame could be invoked with a JSGlobalObject (script execution context) belonging to a different-origin frame, a malicious page could grab another frame’s messageHandlers object (e.g. via iframe.contentWindow.window.webkit.messageHandlers.<name>) and post to a handler intended for a different origin, leaking access to handlers not meant for it.
The fix adds passesSameOriginCheck(globalObject, frame): it takes the calling JSGlobalObject, resolves the caller’s scriptExecutionContext->securityOrigin(), resolves the handler frame’s document()->securityOrigin(), and requires securityOrigin->isSameOriginAs(frameSecurityOrigin), returning false (and rejecting the promise / returning an InvalidAccessError exception) on any mismatch or missing object. Both entry points now perform this check before dispatching to the descriptor. The API signatures were also tightened: postMessage now returns void and rejects the promise directly, while postLegacySynchronousMessage now returns ExceptionOrJSC::JSValue so it can surface the InvalidAccessError. The new test MessageHandlerFromIframe confirms that grabbing a cross-origin iframe’s testhandler1 and posting to it is rejected with ‘InvalidAccessError: Failed same-origin check.’, while a same-origin handler still succeeds.
Attack Path
- Host registers a named message handler An embedding app registers a WKScriptMessageHandler under a name (e.g. testhandler1), expecting only its own trusted content to use it.
- Attacker page loads a cross-origin frame The malicious top-level page (or a frame it controls) creates an iframe pointing at the origin/content for which the handler is intended, or otherwise obtains a reference into that frame.
- Grab the other frame's messageHandlers Script reads iframe.contentWindow.window.webkit.messageHandlers.<name>, obtaining a UserMessageHandler tied to the other frame while the calling script’s origin differs.
- Invoke postMessage cross-origin The attacker calls handler.postMessage(…) so UserMessageHandler::postMessage runs with a JSGlobalObject from the attacker’s origin but a handler bound to another frame’s origin.
- Reach the unintended handler (pre-patch) Without the same-origin check, the message is delivered to the host handler intended for the other origin, letting the attacker drive privileged app-side functionality or spoof messages from a trusted origin.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
passesSameOriginCheckSource/WebCore/page/UserMessageHandler.cpp |
added | New static helper that compares the calling JSGlobalObject's scriptExecutionContext security origin against the handler frame's document security origin, returning false on any mismatch or missing object. |
UserMessageHandler::postMessageSource/WebCore/page/UserMessageHandler.cpp |
modified | Now returns void and rejects the promise directly; adds a passesSameOriginCheck gate that rejects with InvalidAccessError 'Failed same-origin check.' before dispatching to the descriptor. |
UserMessageHandler::postLegacySynchronousMessageSource/WebCore/page/UserMessageHandler.cpp |
modified | Signature changed to ExceptionOr<JSC::JSValue>; returns InvalidAccessError when the descriptor is missing or the same-origin check fails, otherwise dispatches. |
UserMessageHandler (declarations)Source/WebCore/page/UserMessageHandler.h |
modified | Updates postMessage/postLegacySynchronousMessage return types to match the new error handling and marks descriptor() const. |
webkit_dom_dom_window_webkit_message_handlers_post_messageSource/WebKit/WebProcess/InjectedBundle/API/gtk/DOM/WebKitDOMDOMWindow.cpp |
modified | Adapts the GTK DOM binding to postMessage's new void return by dropping the hasException() check. |
MessageHandlerFromIframe testTools/TestWebKitAPI/Tests/WebKit/WKWebView/WKWebViewEvaluateJavaScript.mm |
added | New test proving a cross-origin iframe handler rejects with 'Failed same-origin check.' while a same-origin handler still receives the message. |
Files Changed
Source/WebCore/page/UserMessageHandler.cppSource/WebCore/page/UserMessageHandler.hSource/WebKit/WebProcess/InjectedBundle/API/gtk/DOM/WebKitDOMDOMWindow.cppTools/TestWebKitAPI/Tests/WebKit/WKWebView/WKWebViewEvaluateJavaScript.mm
Audit Directions
- Other UserMessageHandler / descriptor entry pointsIn UserMessageHandler.cpp and UserMessageHandlerDescriptor grep for didPostMessage / didPostLegacySynchronousMessage callers to confirm every dispatch path now flows through passesSameOriginCheck and none bypass it.
- Objects reachable across frames via contentWindowAudit other WebKit-exposed objects fetched through iframe.contentWindow.window.webkit.* or cross-frame property access that act on their owning frame without re-checking the caller’s origin; grep for scriptExecutionContext()->securityOrigin() usage vs. its absence.
- Frame-bound WebCore APIs missing origin checksSearch page/ and bindings for methods that capture a LocalFrame& in their constructor and later use m_frame without comparing to the calling globalObject’s origin (isSameOriginAs), a general tell for this bug class.
- Port bindings assuming old return typesGrep across ports (gtk DOM bindings, JS bindings) for callers of postMessage/postLegacySynchronousMessage to ensure none silently ignore the new InvalidAccessError or reintroduce a path that skips the check.