← All issues

[9] UserMessageHandler.postMessage should fail if called from another frame

Severity: Medium | Component: WebCore page/UserMessageHandler | 795ef8a

Rated Medium because the observable effect is origin-spoofed delivery to a host-app WKScriptMessageHandler from a captured cross-origin reference, and impact depends on host-app trust placed on frameInfo/origin (no memory primitive).

Source/WebCore/page/UserMessageHandler.cpp

+static bool passesSameOriginCheck(JSC::JSGlobalObject& globalObject, RefPtr<LocalFrame> frame)
+{
+ if (!frame) return false;
+ RefPtr document = frame->document();
+ if (!document) return false;
+ Ref frameSecurityOrigin = document->securityOrigin();
+ if (!globalObject.inherits<JSDOMGlobalObject>()) return false;
+ RefPtr scriptExecutionContext = uncheckedDowncast<JSDOMGlobalObject>(globalObject).scriptExecutionContext();
+ if (!scriptExecutionContext) return false;
+ RefPtr securityOrigin = scriptExecutionContext->securityOrigin();
+ if (!securityOrigin) return false;
+ return securityOrigin->isSameOriginAs(frameSecurityOrigin);
+}
+
void UserMessageHandler::postMessage(JSC::JSGlobalObject& globalObject, JSC::JSValue value, Ref<DeferredPromise>&& promise)
{
RefPtr descriptor = m_descriptor;
- if (!descriptor) {
- promise->reject(Exception { ExceptionCode::InvalidAccessError });
- return Exception { ExceptionCode::InvalidAccessError };
- }
+ if (!descriptor)
+ return promise->reject(Exception { ExceptionCode::InvalidAccessError });
+ if (!passesSameOriginCheck(globalObject, m_frame.get()))
+ return promise->reject(Exception { ExceptionCode::InvalidAccessError, "Failed same-origin check."_s });
descriptor->didPostMessage(*this, globalObject, value, ...);
}

A new passesSameOriginCheck helper compares the calling JSGlobalObject's scriptExecutionContext security origin against the handler's owning LocalFrame's current document origin. Both postMessage and postLegacySynchronousMessage invoke it before forwarding to the descriptor. Return types are reshaped: postMessage is now void (rejecting via the promise); postLegacySynchronousMessage returns ExceptionOr<JSC::JSValue>. A new test grabs iframe1.contentWindow.window.webkit.messageHandlers.testhandler1 from a cross-origin iframe and expects the post to fail.

Same-origin invariant enforced at capability-acquisition time but not re-checked at capability-use time, allowing a captured handler reference to be exercised after the underlying frame's origin has changed.

WKUserContentController lets host apps register named JS-callable bridges; in script, window.webkit.messageHandlers.<name>.postMessage(value) delivers value to the app's WKScriptMessageHandler. The UserMessageHandler C++ object backing each name is constructed per-frame and bound to that LocalFrame via FrameDestructionObserver. When two frames are same-origin, scripts in one can directly reach JS objects on the other's global; navigating a frame replaces its Document/origin but does not automatically invalidate JS object references already captured by other scripts.

postMessage checked only that m_descriptor was attached, then forwarded directly to descriptor->didPostMessage(*this, globalObject, ...). The handler is bound at construction to a specific frame, and the host app identifies messages by the handler's frame at delivery time. Nothing on the call path verified that the JSGlobalObject actually invoking postMessage belonged to the same security origin as the frame the handler is attached to.

🔒

Where the same-origin policy is enforced for this capability — and what changes for an attacker who captured a reference at the right moment — is examined in detail.

Subscribe to read more

🔒

Several reusable audit patterns identified for capability-style JS bridges across WebCore, with concrete starting points for variant discovery.

Subscribe to read more