98c97d08a135b120b8e0db74d573f745b1845c77 [Site Isolation] [iOS] imported/w3c/web-platform-tests/html/user-activation/propagation-crossorigin.sub.html fails
Triage note: potentialTapAtPosition/commitPotentialTap now route into a RemoteFrame only via remoteFrameForTapTarget, which hit-tests with AllowVisibleChildFrameContentOnly and requires result.innerNode()==frameOwner && isOverWidget (mirroring EventHandler::subframeForHitTestResult), and uses m_potentialTapLocation. Previously any tap whose node was a remote frame owner was forwarded, allowing user input to be misdelivered into a cross-origin frame in site-isolation.
The bug at a glance
A site-isolation CrossOrigin input-routing bug (iOS/Cocoa tap handling in the WebContent local-root process): potentialTapAtPosition/commitPotentialTap forwarded a tap into a cross-origin RemoteFrame whenever the responding node happened to be the frame’s owner element, without verifying the hit actually landed on the subframe’s content rather than the owner element’s border/padding, and it transformed the raw positionInRootView instead of the touch-adjusted m_potentialTapLocation. A tap meant for the embedding page’s iframe chrome (border/padding) could therefore be delivered into the cross-origin frame, and at mismatched coordinates. Medium severity: this is cross-origin input misdelivery with clickjacking potential, not memory corruption or RCE; the attacker is the embedding page manipulating iframe geometry so a user’s tap is routed/placed where the embedder wants inside a victim cross-origin frame.
Under site isolation the local root process decides whether a tap belongs to a remote subframe purely from “is the hit node a remote frame owner?” — which is true even for the owner’s border/padding — so input on the parent’s own box leaked into the child frame; the fix re-runs the same AllowVisibleChildFrameContentOnly hit test EventHandler uses for real mouse events.
Root cause
The invariant: a tap is routed into a child frame only if it lands on that frame’s content region, exactly as EventHandler::subframeForHitTestResult decides for ordinary mouse events (hit test with AllowVisibleChildFrameContentOnly, require innerNode()==owner and isOverWidget()). Pre-patch, WebPage::potentialTapAtPosition set m_potentialTapNode = localRootFrame->nodeRespondingToClickEvents(positionInRootView, m_potentialTapLocation, …) and then did: RefPtr frameOwner = dynamicDowncast<HTMLFrameOwnerElement>(m_potentialTapNode.get()); if (RefPtr remoteFrame = frameOwner ? dynamicDowncast<RemoteFrame>(frameOwner->contentFrame()) : nullptr) { ... transformer.transformToRemoteFrameCoordinates(positionInRootView) ... }. nodeRespondingToClickEvents can return the frame owner element for a tap on the owner’s border/padding (which are the owner’s box in the embedding document, not subframe content), so any such tap was forwarded to the remote process. commitPotentialTap repeated the same owner-only test. Two defects: (1) no content/over-widget check, so owner-chrome taps misroute cross-origin; (2) the coordinate sent was positionInRootView, the raw pre-adjustment point, whereas m_potentialTapLocation is the touch-adjusted location nodeRespondingToClickEvents computed — a mismatch that places the tap at a different spot inside the remote frame than where hit-testing resolved.
The fix introduces static remoteFrameForTapTarget(localRootFrame, tapTarget, tapLocationInRootView): it confirms the target is an HTMLFrameOwnerElement whose contentFrame() is a RemoteFrame, then hit-tests localRootFrame->eventHandler().hitTestResultAtPoint(view->windowToContents(roundedIntPoint(tapLocationInRootView)), {ReadOnly, Active, AllowVisibleChildFrameContentOnly}) and returns the remote frame only if result.innerNode() == frameOwner && result.isOverWidget(); otherwise nullptr (so the tap is dispatched to the owner element in this frame instead). Both potentialTapAtPosition and commitPotentialTap now call it with m_potentialTapLocation, and the coordinate transform uses m_potentialTapLocation too, aligning the forwarded point with the hit-tested location. These are RefPtr/Node handles in the WebContent process; no lifetime bug — this is an input-routing/origin-isolation correctness fix (m_potentialTapNode is protected across the suspension via protect()).
Key code
Only route into the remote frame when the hit lands on frame content (mirrors subframeForHitTestResult)
static RefPtr<RemoteFrame> remoteFrameForTapTarget(LocalFrame* localRootFrame, Node* tapTarget, const FloatPoint& tapLocationInRootView)
{
RefPtr frameOwner = dynamicDowncast<HTMLFrameOwnerElement>(tapTarget);
RefPtr remoteFrame = frameOwner ? dynamicDowncast<RemoteFrame>(frameOwner->contentFrame()) : nullptr;
if (!remoteFrame || !localRootFrame)
return nullptr;
RefPtr view = localRootFrame->view();
if (!view)
return nullptr;
constexpr OptionSet<HitTestRequest::Type> hitType { HitTestRequest::Type::ReadOnly, HitTestRequest::Type::Active, HitTestRequest::Type::AllowVisibleChildFrameContentOnly };
auto result = localRootFrame->eventHandler().hitTestResultAtPoint(view->windowToContents(roundedIntPoint(tapLocationInRootView)), hitType);
if (result.innerNode() != frameOwner || !result.isOverWidget())
return nullptr;
return remoteFrame;
}
Fix approach
The fix funnels both the potential-tap and the commit-tap remote-routing decisions through a single remoteFrameForTapTarget() helper that reproduces EventHandler::subframeForHitTestResult’s logic: a read-only/active hit test with AllowVisibleChildFrameContentOnly, routing into the RemoteFrame only when the hit node is exactly the frame owner and the point is over the widget’s content, so taps on owner border/padding fall through to normal in-frame dispatch. It also switches the forwarded coordinate from the raw positionInRootView to m_potentialTapLocation, so the coordinate handed to the remote process matches the location that hit-testing actually resolved. commitPotentialTap uses this->localRootFrame(frameID) as the root, keeping the two phases consistent. The result is that a cross-origin remote frame receives a tap only for points genuinely inside its content box and at the correct location.
Patch walkthrough
Source/WebKit/WebProcess/WebPage/Cocoa/WebPageCocoa.mm— Adds static remoteFrameForTapTarget() doing the AllowVisibleChildFrameContentOnly hit test with innerNode==owner && isOverWidget; rewrites potentialTapAtPosition and commitPotentialTap to use it and to transform/forward m_potentialTapLocation instead of the raw positionInRootView.
Background
Site isolation / RemoteFrame — With site isolation, a cross-origin iframe is a RemoteFrame in the embedding (local root) process — a stand-in whose real content lives in another process; input that belongs to it must be forwarded over IPC as RemoteUserInputEventData.
HTMLFrameOwnerElement — The embedding element (iframe/object) in the parent document; its CSS box includes border and padding that belong to the parent document, while only the content area maps to the child frame.
nodeRespondingToClickEvents vs hit test with AllowVisibleChildFrameContentOnly — nodeRespondingToClickEvents can resolve to the frame owner for taps anywhere on its box; AllowVisibleChildFrameContentOnly plus isOverWidget() is how EventHandler distinguishes a hit on actual child-frame content from a hit on the owner’s chrome.
m_potentialTapLocation vs positionInRootView — positionInRootView is the raw tap point; m_potentialTapLocation is the touch-adjusted location nodeRespondingToClickEvents produced. Forwarding the raw point placed the tap at a different spot in the remote frame than hit-testing chose.
isOverWidget() — A HitTestResult flag indicating the point is over a widget (frame) content region rather than the owner element’s non-content box; required alongside innerNode()==owner to route into the frame.
Vulnerability window
- Setup — Embedding page hosts a cross-origin iframe (a RemoteFrame under site isolation) and styles it with border/padding or positions it to overlap intended tap targets.
- Tap — User taps; WebPage::potentialTapAtPosition resolves m_potentialTapNode via nodeRespondingToClickEvents.
- Misroute (pre-patch) — Because the node is the frame owner (even for a border/padding hit), the tap is forwarded to the remote frame’s process, transformed from the raw positionInRootView.
- Deliver — The cross-origin frame receives a tap it should not have, possibly at a coordinate the user did not aim at.
- Post-patch — remoteFrameForTapTarget’s content hit test rejects owner-chrome taps and uses m_potentialTapLocation, so only genuine content taps route in.
Proof of concept
No regression test is in the diff. Conceptual trigger (requires site isolation + touch/iOS tap path): embed a cross-origin iframe with a thick border/padding, e.g. , overlay a lure so the user taps on the border/padding region; pre-patch the tap is routed into victim.example (and at a shifted coordinate). The exact cross-origin consequence is (inferred) and depends on what the victim frame does with the received tap.
Exploitation
- Reach — An embedding page controlling iframe geometry/CSS, on a site-isolation build with the iOS/Cocoa tap path.
- Trigger — Induce a user tap on the iframe owner’s border/padding (or rely on the coordinate mismatch) so the pre-patch owner-only test forwards it to the remote frame.
- Primitive — Cross-origin input injection: a tap/click delivered into a victim cross-origin frame at an embedder-influenced location (clickjacking-class).
- Escalation — Depends on the victim frame’s UI (e.g. a consent/one-tap button at the forwarded coordinate); whether a single routed tap can complete a sensitive action gates escalation and is (inferred).
Detection & hunting
For defenders and detection engineers:
- Remote-frame tap routing without an over-widget content hit — Log potentialTapAtPosition/commitPotentialTap forwarding where a fresh AllowVisibleChildFrameContentOnly hit test would not yield innerNode==owner && isOverWidget.
- Forwarded coordinate vs hit-test coordinate divergence — Compare the transformed positionInRootView against m_potentialTapLocation to spot pre-fix coordinate mismatches.
- Taps into cross-origin frames on owner border/padding — Instrument RemoteUserInputEventData emission with the owner element’s content-box geometry to flag out-of-content hits.
Remaining risk in the patch
- Re-hit-test vs original-tap consistency — remoteFrameForTapTarget runs a fresh hit test at commit time (and in commitPotentialTap uses this->localRootFrame(frameID)); if layout changed between potential-tap and commit, the content check and the original m_potentialTapNode could disagree, a race not addressed by the diff.
- Platform scope — The fix is in WebPageCocoa.mm (iOS tap path); whether every other platform’s tap/click-into-remote-frame routing has the equivalent content hit test is not shown by this diff.
Audit directions
- Other remote-frame input forwarding paths — Audit mouse, pointer, wheel, and gesture routing into RemoteFrames to confirm each uses a content hit test (subframeForHitTestResult-equivalent) rather than owner-element identity alone.
- Coordinate provenance in forwarded events — Check every RemoteFrameGeometryTransformer use that it transforms the hit-tested/adjusted location, not a raw input point.
- nodeRespondingToClickEvents consumers — Find other code that treats a frame-owner result from nodeRespondingToClickEvents as ’tap is inside the frame’ without an over-widget check.
- Non-Cocoa tap/click commit paths — Verify the analogous GTK/WPE/other-platform tap-commit routing applies the same content-only gating under site isolation.