WKUserScripts injecting into all frames seem to violate app-bound domains
https://bugs.webkit.org/show_bug.cgi?id=213816
<rdar://problem/64872296>

Reviewed by Brady Eidson.

Source/WebKit:

Patch to make app-bound domain checks on a per-frame basis instead
of per-page-based-on-main-frame. This will prevent WKUserScripts from
being injected in subframes when the main frame is app-bound. Now, all
ancestors of a frame must be app-bound in order for a frame to
evaluate user script.

* UIProcess/WebPageProxy.cpp:
(WebKit::WebPageProxy::setIsNavigatingToAppBoundDomainAndCheckIfPermitted):
Move 'isMainFrame' check to be inside of the check for
m_limitsNavigationsToAppBoundDomains. The navigation should fail if
the main frame is not app-bound and the WKWebView is marked with the
limitsNavigationToAppBoundDomains flag, but non-app-bound subframe
loads are allowed to continue.

(WebKit::WebPageProxy::decidePolicyForNavigationAction):
* UIProcess/WebPageProxy.h:
(WebKit::WebPageProxy::isTopFrameNavigatingToAppBoundDomain const):
* UIProcess/WebProcessPool.cpp:
(WebKit::WebProcessPool::download):
* WebProcess/Network/WebLoaderStrategy.cpp:
(WebKit::WebLoaderStrategy::scheduleLoadFromNetworkProcess):
(WebKit::WebLoaderStrategy::loadResourceSynchronously):
(WebKit::WebLoaderStrategy::startPingLoad):
(WebKit::WebLoaderStrategy::preconnectTo):
For reporting loads to the Network Process, we only care if the main
frame is app bound, so we need to store that info in the WebPage and
WebPageProxy.

* WebProcess/WebCoreSupport/WebFrameLoaderClient.cpp:
(WebKit::WebFrameLoaderClient::shouldEnableInAppBrowserPrivacyProtections const):
* WebProcess/WebPage/WebFrame.cpp:
(WebKit::WebFrame::startDownload):
(WebKit::WebFrame::convertMainResourceLoadToDownload):
(WebKit::WebFrame::shouldEnableInAppBrowserPrivacyProtections):
(WebKit::WebFrame::isTopFrameNavigatingToAppBoundDomain const):
* WebProcess/WebPage/WebFrame.h:
(WebKit::WebFrame::setIsNavigatingToAppBoundDomain):
(WebKit::WebFrame::isNavigatingToAppBoundDomain const):
* WebProcess/WebPage/WebPage.cpp:
(WebKit::WebPage::loadRequest):
(WebKit::WebPage::loadDataImpl):
(WebKit::WebPage::didReceivePolicyDecision):
(WebKit::WebPage::runJavaScript):
(WebKit::WebPage::setIsNavigatingToAppBoundDomain):
(WebKit::WebPage::shouldEnableInAppBrowserPrivacyProtections): Deleted.
* WebProcess/WebPage/WebPage.h:
(WebKit::WebPage::needsInAppBrowserPrivacyQuirks):
(WebKit::WebPage::isNavigatingToAppBoundDomain const): Deleted.
Move all app-bound domain logic from the WebPage to the WebFrame now
that checks are on a per-frame basis.

Tools:

Added API test coverage. Sets up a tree like so: [app-bound domain,
non-app-bound domain, app-bound domain]. Script injection should only
be successful in the top frame. Since message handlers are disabled for non-
app-bound domains, this test injects script which loads an image using a
custom url scheme. If the load succeeds, we increment a counter, and
expect only one successful script load (the main frame).

* TestWebKitAPI/Tests/WebKitCocoa/InAppBrowserPrivacy.mm:
(-[InAppBrowserSchemeHandler webView:startURLSchemeTask:]):


git-svn-id: http://svn.webkit.org/repository/webkit/trunk@263806 268f45cc-cd09-0410-ab3c-d52691b4dbfc
diff --git a/Source/WebKit/ChangeLog b/Source/WebKit/ChangeLog
index d4759ed..34bcf1c 100644
--- a/Source/WebKit/ChangeLog
+++ b/Source/WebKit/ChangeLog
@@ -1,3 +1,62 @@
+2020-07-01  Kate Cheney  <katherine_cheney@apple.com>
+
+        WKUserScripts injecting into all frames seem to violate app-bound domains
+        https://bugs.webkit.org/show_bug.cgi?id=213816
+        <rdar://problem/64872296>
+
+        Reviewed by Brady Eidson.
+
+        Patch to make app-bound domain checks on a per-frame basis instead
+        of per-page-based-on-main-frame. This will prevent WKUserScripts from
+        being injected in subframes when the main frame is app-bound. Now, all
+        ancestors of a frame must be app-bound in order for a frame to
+        evaluate user script.
+
+        * UIProcess/WebPageProxy.cpp:
+        (WebKit::WebPageProxy::setIsNavigatingToAppBoundDomainAndCheckIfPermitted):
+        Move 'isMainFrame' check to be inside of the check for
+        m_limitsNavigationsToAppBoundDomains. The navigation should fail if
+        the main frame is not app-bound and the WKWebView is marked with the
+        limitsNavigationToAppBoundDomains flag, but non-app-bound subframe
+        loads are allowed to continue.
+
+        (WebKit::WebPageProxy::decidePolicyForNavigationAction):
+        * UIProcess/WebPageProxy.h:
+        (WebKit::WebPageProxy::isTopFrameNavigatingToAppBoundDomain const):
+        * UIProcess/WebProcessPool.cpp:
+        (WebKit::WebProcessPool::download):
+        * WebProcess/Network/WebLoaderStrategy.cpp:
+        (WebKit::WebLoaderStrategy::scheduleLoadFromNetworkProcess):
+        (WebKit::WebLoaderStrategy::loadResourceSynchronously):
+        (WebKit::WebLoaderStrategy::startPingLoad):
+        (WebKit::WebLoaderStrategy::preconnectTo):
+        For reporting loads to the Network Process, we only care if the main
+        frame is app bound, so we need to store that info in the WebPage and
+        WebPageProxy.
+
+        * WebProcess/WebCoreSupport/WebFrameLoaderClient.cpp:
+        (WebKit::WebFrameLoaderClient::shouldEnableInAppBrowserPrivacyProtections const):
+        * WebProcess/WebPage/WebFrame.cpp:
+        (WebKit::WebFrame::startDownload):
+        (WebKit::WebFrame::convertMainResourceLoadToDownload):
+        (WebKit::WebFrame::shouldEnableInAppBrowserPrivacyProtections):
+        (WebKit::WebFrame::isTopFrameNavigatingToAppBoundDomain const):
+        * WebProcess/WebPage/WebFrame.h:
+        (WebKit::WebFrame::setIsNavigatingToAppBoundDomain):
+        (WebKit::WebFrame::isNavigatingToAppBoundDomain const):
+        * WebProcess/WebPage/WebPage.cpp:
+        (WebKit::WebPage::loadRequest):
+        (WebKit::WebPage::loadDataImpl):
+        (WebKit::WebPage::didReceivePolicyDecision):
+        (WebKit::WebPage::runJavaScript):
+        (WebKit::WebPage::setIsNavigatingToAppBoundDomain):
+        (WebKit::WebPage::shouldEnableInAppBrowserPrivacyProtections): Deleted.
+        * WebProcess/WebPage/WebPage.h:
+        (WebKit::WebPage::needsInAppBrowserPrivacyQuirks):
+        (WebKit::WebPage::isNavigatingToAppBoundDomain const): Deleted.
+        Move all app-bound domain logic from the WebPage to the WebFrame now
+        that checks are on a per-frame basis.
+
 2020-07-01  Youenn Fablet  <youenn@apple.com>
 
         Make NetworkSendQueue use CString instead of String for UTF-8 data
diff --git a/Source/WebKit/UIProcess/WebPageProxy.cpp b/Source/WebKit/UIProcess/WebPageProxy.cpp
index 89f4a8dd..6b400dc 100644
--- a/Source/WebKit/UIProcess/WebPageProxy.cpp
+++ b/Source/WebKit/UIProcess/WebPageProxy.cpp
@@ -3143,31 +3143,34 @@
 bool WebPageProxy::setIsNavigatingToAppBoundDomainAndCheckIfPermitted(bool isMainFrame, const URL& requestURL, Optional<NavigatingToAppBoundDomain> isNavigatingToAppBoundDomain)
 {
 #if PLATFORM(IOS_FAMILY)
-    if (isMainFrame) {
-        if (WEB_PAGE_PROXY_ADDITIONS_SETISNAVIGATINGTOAPPBOUNDDOMAIN)
-            return true;
-        if (!isNavigatingToAppBoundDomain) {
-            m_isNavigatingToAppBoundDomain = WTF::nullopt;
-            return true;
-        }
-        if (m_ignoresAppBoundDomains)
-            return true;
-        
-        if (shouldTreatURLProtocolAsAppBound(requestURL)) {
-            isNavigatingToAppBoundDomain = NavigatingToAppBoundDomain::Yes;
-            m_limitsNavigationsToAppBoundDomains = true;
-        }
-        if (m_limitsNavigationsToAppBoundDomains) {
-            if (*isNavigatingToAppBoundDomain == NavigatingToAppBoundDomain::No)
-                return false;
-            m_configuration->setWebViewCategory(WebViewCategory::AppBoundDomain);
-            m_isNavigatingToAppBoundDomain = NavigatingToAppBoundDomain::Yes;
-        } else {
-            if (m_hasExecutedAppBoundBehaviorBeforeNavigation)
+    if (WEB_PAGE_PROXY_ADDITIONS_SETISNAVIGATINGTOAPPBOUNDDOMAIN)
+        return true;
+    if (!isNavigatingToAppBoundDomain) {
+        m_isNavigatingToAppBoundDomain = WTF::nullopt;
+        return true;
+    }
+    if (m_ignoresAppBoundDomains)
+        return true;
+
+    if (shouldTreatURLProtocolAsAppBound(requestURL)) {
+        isNavigatingToAppBoundDomain = NavigatingToAppBoundDomain::Yes;
+        m_limitsNavigationsToAppBoundDomains = true;
+    }
+    if (m_limitsNavigationsToAppBoundDomains) {
+        if (*isNavigatingToAppBoundDomain == NavigatingToAppBoundDomain::No) {
+            if (isMainFrame)
                 return false;
             m_configuration->setWebViewCategory(WebViewCategory::InAppBrowser);
             m_isNavigatingToAppBoundDomain = NavigatingToAppBoundDomain::No;
+            return true;
         }
+        m_configuration->setWebViewCategory(WebViewCategory::AppBoundDomain);
+        m_isNavigatingToAppBoundDomain = NavigatingToAppBoundDomain::Yes;
+    } else {
+        if (m_hasExecutedAppBoundBehaviorBeforeNavigation)
+            return false;
+        m_configuration->setWebViewCategory(WebViewCategory::InAppBrowser);
+        m_isNavigatingToAppBoundDomain = NavigatingToAppBoundDomain::No;
     }
 #else
     UNUSED_PARAM(isMainFrame);
@@ -5214,6 +5217,8 @@
                 completionHandler(PolicyAction::Ignore);
                 return;
             }
+            if (frame->isMainFrame())
+                m_isTopFrameNavigatingToAppBoundDomain = m_isNavigatingToAppBoundDomain;
         }
 #endif
 
diff --git a/Source/WebKit/UIProcess/WebPageProxy.h b/Source/WebKit/UIProcess/WebPageProxy.h
index 3c35aea..f8ce508 100644
--- a/Source/WebKit/UIProcess/WebPageProxy.h
+++ b/Source/WebKit/UIProcess/WebPageProxy.h
@@ -1759,6 +1759,7 @@
     void isForcedIntoAppBoundModeTesting(CompletionHandler<void(bool)>&&);
 
     Optional<NavigatingToAppBoundDomain> isNavigatingToAppBoundDomain() const { return m_isNavigatingToAppBoundDomain; }
+    Optional<NavigatingToAppBoundDomain> isTopFrameNavigatingToAppBoundDomain() const { return m_isTopFrameNavigatingToAppBoundDomain; }
 
 #if PLATFORM(COCOA)
     WebCore::ResourceError errorForUnpermittedAppBoundDomainNavigation(const URL&);
@@ -2837,6 +2838,7 @@
 #endif
         
     Optional<NavigatingToAppBoundDomain> m_isNavigatingToAppBoundDomain;
+    Optional<NavigatingToAppBoundDomain> m_isTopFrameNavigatingToAppBoundDomain;
     bool m_ignoresAppBoundDomains { false };
     bool m_userScriptsNotified { false };
     bool m_limitsNavigationsToAppBoundDomains { false };
diff --git a/Source/WebKit/UIProcess/WebProcessPool.cpp b/Source/WebKit/UIProcess/WebProcessPool.cpp
index 2b27694..20cf926 100644
--- a/Source/WebKit/UIProcess/WebProcessPool.cpp
+++ b/Source/WebKit/UIProcess/WebProcessPool.cpp
@@ -1389,7 +1389,7 @@
     Optional<NavigatingToAppBoundDomain> isAppBound = NavigatingToAppBoundDomain::No;
     if (initiatingPage) {
         initiatingPage->handleDownloadRequest(downloadProxy);
-        isAppBound = initiatingPage->isNavigatingToAppBoundDomain();
+        isAppBound = initiatingPage->isTopFrameNavigatingToAppBoundDomain();
     }
 
     if (networkProcess()) {
diff --git a/Source/WebKit/WebProcess/Network/WebLoaderStrategy.cpp b/Source/WebKit/WebProcess/Network/WebLoaderStrategy.cpp
index 6a7eca6..1f0e5e9 100644
--- a/Source/WebKit/WebProcess/Network/WebLoaderStrategy.cpp
+++ b/Source/WebKit/WebProcess/Network/WebLoaderStrategy.cpp
@@ -337,8 +337,8 @@
     auto* webFrameLoaderClient = frame ? toWebFrameLoaderClient(frame->loader().client()) : nullptr;
     auto* webFrame = webFrameLoaderClient ? &webFrameLoaderClient->webFrame() : nullptr;
     auto* webPage = webFrame ? webFrame->page() : nullptr;
-    if (webPage)
-        loadParameters.isNavigatingToAppBoundDomain = webPage->isNavigatingToAppBoundDomain();
+    if (webFrame)
+        loadParameters.isNavigatingToAppBoundDomain = webFrame->isTopFrameNavigatingToAppBoundDomain();
 
 #if ENABLE(CONTENT_EXTENSIONS)
     if (document) {
@@ -603,9 +603,8 @@
     }
     loadParameters.originalRequestHeaders = originalRequestHeaders;
     
-    if (webPage)
-        loadParameters.isNavigatingToAppBoundDomain = webPage->isNavigatingToAppBoundDomain();
-
+    if (webFrame)
+        loadParameters.isNavigatingToAppBoundDomain = webFrame->isTopFrameNavigatingToAppBoundDomain();
     addParametersShared(webFrame->coreFrame(), loadParameters);
 
     data.shrink(0);
@@ -685,7 +684,7 @@
     }
     addParametersShared(&frame, loadParameters);
     
-    loadParameters.isNavigatingToAppBoundDomain = webPage->isNavigatingToAppBoundDomain();
+    loadParameters.isNavigatingToAppBoundDomain = webFrame->isTopFrameNavigatingToAppBoundDomain();
     
 #if ENABLE(CONTENT_EXTENSIONS)
     loadParameters.mainDocumentURL = document->topDocument().url();
@@ -753,7 +752,7 @@
     parameters.shouldRestrictHTTPResponseAccess = shouldPerformSecurityChecks();
     // FIXME: Use the proper destination once all fetch options are passed.
     parameters.options.destination = FetchOptions::Destination::EmptyString;
-    parameters.isNavigatingToAppBoundDomain = webPage.isNavigatingToAppBoundDomain();
+    parameters.isNavigatingToAppBoundDomain = webFrame.isTopFrameNavigatingToAppBoundDomain();
 
     WebProcess::singleton().ensureNetworkProcessConnection().connection().send(Messages::NetworkConnectionToWebProcess::PreconnectTo(preconnectionIdentifier, WTFMove(parameters)), 0);
 }
diff --git a/Source/WebKit/WebProcess/WebCoreSupport/WebFrameLoaderClient.cpp b/Source/WebKit/WebProcess/WebCoreSupport/WebFrameLoaderClient.cpp
index 0587c38..c975bf1 100644
--- a/Source/WebKit/WebProcess/WebCoreSupport/WebFrameLoaderClient.cpp
+++ b/Source/WebKit/WebProcess/WebCoreSupport/WebFrameLoaderClient.cpp
@@ -1912,14 +1912,7 @@
 
 bool WebFrameLoaderClient::shouldEnableInAppBrowserPrivacyProtections() const
 {
-    if (!m_frame->isMainFrame())
-        return false;
-
-    auto* webPage = m_frame->page();
-    if (!webPage)
-        return false;
-
-    return webPage->shouldEnableInAppBrowserPrivacyProtections();
+    return m_frame->shouldEnableInAppBrowserPrivacyProtections();
 }
 
 void WebFrameLoaderClient::notifyPageOfAppBoundBehavior()
diff --git a/Source/WebKit/WebProcess/WebPage/WebFrame.cpp b/Source/WebKit/WebProcess/WebPage/WebFrame.cpp
index 9d76794..4b7a77b 100644
--- a/Source/WebKit/WebProcess/WebPage/WebFrame.cpp
+++ b/Source/WebKit/WebProcess/WebPage/WebFrame.cpp
@@ -288,9 +288,7 @@
     m_policyDownloadID = { };
 
     Optional<NavigatingToAppBoundDomain> isAppBound = NavigatingToAppBoundDomain::No;
-    if (page())
-        isAppBound = page()->isNavigatingToAppBoundDomain();
-    
+    isAppBound = m_isNavigatingToAppBoundDomain;
     WebProcess::singleton().ensureNetworkProcessConnection().connection().send(Messages::NetworkConnectionToWebProcess::StartDownload(policyDownloadID, request,  isAppBound, suggestedName), 0);
 }
 
@@ -314,9 +312,7 @@
         mainResourceLoadIdentifier = 0;
 
     Optional<NavigatingToAppBoundDomain> isAppBound = NavigatingToAppBoundDomain::No;
-    if (page())
-        isAppBound = page()->isNavigatingToAppBoundDomain();
-        
+    isAppBound = m_isNavigatingToAppBoundDomain;
     webProcess.ensureNetworkProcessConnection().connection().send(Messages::NetworkConnectionToWebProcess::ConvertMainResourceLoadToDownload(mainResourceLoadIdentifier, policyDownloadID, request, response, isAppBound), 0);
 }
 
@@ -858,5 +854,28 @@
 
     return sharedSnapshot;
 }
+
+bool WebFrame::shouldEnableInAppBrowserPrivacyProtections()
+{
+    if (page() && page()->needsInAppBrowserPrivacyQuirks())
+        return false;
+
+    bool treeHasNonAppBoundFrame = m_isNavigatingToAppBoundDomain && m_isNavigatingToAppBoundDomain == NavigatingToAppBoundDomain::No;
+    if (!treeHasNonAppBoundFrame) {
+        for (WebFrame* frame = this; !frame->isMainFrame(); frame = frame->parentFrame()) {
+            if (frame->isNavigatingToAppBoundDomain() && frame->isNavigatingToAppBoundDomain() == NavigatingToAppBoundDomain::No) {
+                treeHasNonAppBoundFrame = true;
+                break;
+            }
+        }
+    }
+    return treeHasNonAppBoundFrame;
+}
+
+Optional<NavigatingToAppBoundDomain> WebFrame::isTopFrameNavigatingToAppBoundDomain() const
+{
+    return fromCoreFrame(m_coreFrame->mainFrame())->isNavigatingToAppBoundDomain();
+}
+
     
 } // namespace WebKit
diff --git a/Source/WebKit/WebProcess/WebPage/WebFrame.h b/Source/WebKit/WebProcess/WebPage/WebFrame.h
index da02921..498a665 100644
--- a/Source/WebKit/WebProcess/WebPage/WebFrame.h
+++ b/Source/WebKit/WebProcess/WebPage/WebFrame.h
@@ -172,6 +172,10 @@
 #endif
 
     WebFrameLoaderClient* frameLoaderClient() const;
+    bool shouldEnableInAppBrowserPrivacyProtections();
+    void setIsNavigatingToAppBoundDomain(Optional<NavigatingToAppBoundDomain> isNavigatingToAppBoundDomain) { m_isNavigatingToAppBoundDomain = isNavigatingToAppBoundDomain; };
+    Optional<NavigatingToAppBoundDomain> isNavigatingToAppBoundDomain() const { return m_isNavigatingToAppBoundDomain; }
+    Optional<NavigatingToAppBoundDomain> isTopFrameNavigatingToAppBoundDomain() const;
 
 private:
     WebFrame();
@@ -192,6 +196,8 @@
 #if PLATFORM(IOS_FAMILY)
     TransactionID m_firstLayerTreeTransactionIDAfterDidCommitLoad;
 #endif
+    Optional<NavigatingToAppBoundDomain> m_isNavigatingToAppBoundDomain;
+
 };
 
 } // namespace WebKit
diff --git a/Source/WebKit/WebProcess/WebPage/WebPage.cpp b/Source/WebKit/WebProcess/WebPage/WebPage.cpp
index 779668b..488006f 100644
--- a/Source/WebKit/WebProcess/WebPage/WebPage.cpp
+++ b/Source/WebKit/WebProcess/WebPage/WebPage.cpp
@@ -1534,7 +1534,7 @@
 
 void WebPage::loadRequest(LoadParameters&& loadParameters)
 {
-    setIsNavigatingToAppBoundDomain(loadParameters.isNavigatingToAppBoundDomain);
+    setIsNavigatingToAppBoundDomain(loadParameters.isNavigatingToAppBoundDomain, &m_mainFrame.get());
 
     SendStopResponsivenessTimer stopper;
 
@@ -1572,7 +1572,7 @@
 
 void WebPage::loadDataImpl(uint64_t navigationID, bool shouldTreatAsContinuingLoad, Optional<WebsitePoliciesData>&& websitePolicies, Ref<SharedBuffer>&& sharedBuffer, const String& MIMEType, const String& encodingName, const URL& baseURL, const URL& unreachableURL, const UserData& userData, Optional<NavigatingToAppBoundDomain> isNavigatingToAppBoundDomain, ShouldOpenExternalURLsPolicy shouldOpenExternalURLsPolicy)
 {
-    setIsNavigatingToAppBoundDomain(isNavigatingToAppBoundDomain);
+    setIsNavigatingToAppBoundDomain(isNavigatingToAppBoundDomain, &m_mainFrame.get());
 
     SendStopResponsivenessTimer stopper;
 
@@ -3284,11 +3284,11 @@
 
 void WebPage::didReceivePolicyDecision(FrameIdentifier frameID, uint64_t listenerID, PolicyDecision&& policyDecision)
 {
-    setIsNavigatingToAppBoundDomain(policyDecision.isNavigatingToAppBoundDomain);
-
     WebFrame* frame = WebProcess::singleton().webFrame(frameID);
     if (!frame)
         return;
+
+    setIsNavigatingToAppBoundDomain(policyDecision.isNavigatingToAppBoundDomain, frame);
     frame->didReceivePolicyDecision(listenerID, WTFMove(policyDecision));
 }
 
@@ -3457,7 +3457,7 @@
 
         send(Messages::WebPageProxy::ScriptValueCallback(dataReference, details, callbackID));
     };
-    if (shouldEnableInAppBrowserPrivacyProtections()) {
+    if (frame->shouldEnableInAppBrowserPrivacyProtections()) {
         send(Messages::WebPageProxy::ScriptValueCallback({ }, ExceptionDetails { "Unable to execute JavaScript in a frame that is not in an app-bound domain"_s, 0, 0, ExceptionDetails::Type::AppBoundDomain }, callbackID));
         if (auto* document = m_page->mainFrame().document())
             document->addConsoleMessage(MessageSource::Security, MessageLevel::Warning, "Ignoring user script injection for non-app bound domain.");
@@ -7162,9 +7162,9 @@
 
 #endif
 
-void WebPage::setIsNavigatingToAppBoundDomain(Optional<NavigatingToAppBoundDomain> isNavigatingToAppBoundDomain)
+void WebPage::setIsNavigatingToAppBoundDomain(Optional<NavigatingToAppBoundDomain> isNavigatingToAppBoundDomain, WebFrame* frame)
 {
-    m_isNavigatingToAppBoundDomain = isNavigatingToAppBoundDomain;
+    frame->setIsNavigatingToAppBoundDomain(isNavigatingToAppBoundDomain);
     
     m_navigationHasOccured = true;
 }
@@ -7175,14 +7175,6 @@
         send(Messages::WebPageProxy::SetHasExecutedAppBoundBehaviorBeforeNavigation());
 }
 
-bool WebPage::shouldEnableInAppBrowserPrivacyProtections()
-{
-    if (m_needsInAppBrowserPrivacyQuirks)
-        return false;
-
-    return isNavigatingToAppBoundDomain() && isNavigatingToAppBoundDomain() == NavigatingToAppBoundDomain::No;
-}
-
 } // namespace WebKit
 
 #undef RELEASE_LOG_IF_ALLOWED
diff --git a/Source/WebKit/WebProcess/WebPage/WebPage.h b/Source/WebKit/WebProcess/WebPage/WebPage.h
index f94acaaf..9204186 100644
--- a/Source/WebKit/WebProcess/WebPage/WebPage.h
+++ b/Source/WebKit/WebProcess/WebPage/WebPage.h
@@ -1326,10 +1326,9 @@
     void getAllFrames(CompletionHandler<void(FrameTreeNodeData&&)>&&);
 
     void notifyPageOfAppBoundBehavior();
-    bool shouldEnableInAppBrowserPrivacyProtections();
-    void setIsNavigatingToAppBoundDomain(Optional<NavigatingToAppBoundDomain>);
-    Optional<NavigatingToAppBoundDomain> isNavigatingToAppBoundDomain() const { return m_isNavigatingToAppBoundDomain; }
-    
+    void setIsNavigatingToAppBoundDomain(Optional<NavigatingToAppBoundDomain>, WebFrame*);
+    bool needsInAppBrowserPrivacyQuirks() { return m_needsInAppBrowserPrivacyQuirks; }
+
     bool shouldUseRemoteRenderingFor(WebCore::RenderingPurpose);
 
 #if ENABLE(MEDIA_USAGE)
@@ -2130,7 +2129,6 @@
     String m_themeName;
 #endif
     
-    Optional<NavigatingToAppBoundDomain> m_isNavigatingToAppBoundDomain;
     bool m_limitsNavigationsToAppBoundDomains { false };
     bool m_navigationHasOccured { false };
 };
diff --git a/Tools/ChangeLog b/Tools/ChangeLog
index 256c75d..111a114 100644
--- a/Tools/ChangeLog
+++ b/Tools/ChangeLog
@@ -1,3 +1,21 @@
+2020-07-01  Kate Cheney  <katherine_cheney@apple.com>
+
+        WKUserScripts injecting into all frames seem to violate app-bound domains
+        https://bugs.webkit.org/show_bug.cgi?id=213816
+        <rdar://problem/64872296>
+
+        Reviewed by Brady Eidson.
+
+        Added API test coverage. Sets up a tree like so: [app-bound domain,
+        non-app-bound domain, app-bound domain]. Script injection should only
+        be successful in the top frame. Since message handlers are disabled for non-
+        app-bound domains, this test injects script which loads an image using a
+        custom url scheme. If the load succeeds, we increment a counter, and
+        expect only one successful script load (the main frame).
+
+        * TestWebKitAPI/Tests/WebKitCocoa/InAppBrowserPrivacy.mm:
+        (-[InAppBrowserSchemeHandler webView:startURLSchemeTask:]):
+
 2020-06-30  Sam Weinig  <weinig@apple.com>
 
         Split Color serialization out of Color classes
diff --git a/Tools/TestWebKitAPI/Tests/WebKitCocoa/InAppBrowserPrivacy.mm b/Tools/TestWebKitAPI/Tests/WebKitCocoa/InAppBrowserPrivacy.mm
index 44c3b36..5c0c1eb 100644
--- a/Tools/TestWebKitAPI/Tests/WebKitCocoa/InAppBrowserPrivacy.mm
+++ b/Tools/TestWebKitAPI/Tests/WebKitCocoa/InAppBrowserPrivacy.mm
@@ -34,6 +34,7 @@
 #import <WebCore/RuntimeApplicationChecks.h>
 #import <WebKit/WKHTTPCookieStorePrivate.h>
 #import <WebKit/WKPreferencesPrivate.h>
+#import <WebKit/WKURLSchemeTaskPrivate.h>
 #import <WebKit/WKUserContentControllerPrivate.h>
 #import <WebKit/WKWebsiteDataStorePrivate.h>
 #import <WebKit/_WKUserContentWorld.h>
@@ -83,6 +84,8 @@
 @end
 
 static NSString * const userScriptSource = @"window.wkUserScriptInjected = true";
+static bool mainFrameReceivedScriptSource = false;
+static bool subFrameReceivedScriptSource = false;
 
 @interface InAppBrowserSchemeHandler : NSObject <WKURLSchemeHandler>
 @end
@@ -93,7 +96,7 @@
 {
     NSString *response = nil;
     if ([task.request.URL.path isEqualToString:@"/in-app-browser-privacy-test-user-script"])
-        response = @"<script>window.wkUserScriptInjected = false;</script>";
+        response = @"<body id = 'body'><script>window.wkUserScriptInjected = false;</script>";
     else if ([task.request.URL.path isEqualToString:@"/in-app-browser-privacy-test-user-agent-script"])
         response = @"<script> window.wkUserScriptInjected = true; </script>";
     else if ([task.request.URL.path isEqualToString:@"/in-app-browser-privacy-test-user-style-sheets"])
@@ -104,6 +107,14 @@
         response = @"<body style='background-color: green;'></body><script>if (window.webkit.messageHandlers)\nwindow.webkit.messageHandlers.testHandler.postMessage('Failed'); \nelse \n document.body.style.background = 'red';</script>";
     else if ([task.request.URL.path isEqualToString:@"/app-bound-domain-load"])
         response = @"<body></body>";
+    else if ([task.request.URL.path isEqualToString:@"/in-app-browser-privacy-test-user-script-iframe"])
+        response = @"<body id = 'body'></body><iframe src='in-app-browser://apple.com/in-app-browser-privacy-test-user-script' id='nestedFrame'></iframe></body>";
+    else if ([task.request.URL.path isEqualToString:@"/in-app-browser-privacy-test-user-script-nested-iframe"])
+        response = @"<body id = 'body'></body><iframe src='in-app-browser://nonAppBoundDomain/in-app-browser-privacy-test-user-script-iframe' id='iframe'></iframe></body>";
+    else if (((id<WKURLSchemeTaskPrivate>)task)._frame.isMainFrame && [task.request.URL.path isEqualToString:@"/should-load-for-main-frame-only"])
+        mainFrameReceivedScriptSource = true;
+    else if ([task.request.URL.path isEqualToString:@"/should-load-for-main-frame-only"])
+        subFrameReceivedScriptSource = true;
 
     [task didReceiveResponse:[[[NSURLResponse alloc] initWithURL:task.request.URL MIMEType:@"text/html" expectedContentLength:response.length textEncodingName:nil] autorelease]];
     [task didReceiveData:[response dataUsingEncoding:NSUTF8StringEncoding]];
@@ -1057,6 +1068,115 @@
     EXPECT_EQ(error.code, WKErrorNavigationAppBoundDomain);
 }
 
+TEST(InAppBrowserPrivacy, InjectScriptInNonAppBoundSubframeAppBoundMainframeFails)
+{
+    isDone = false;
+    initializeInAppBrowserPrivacyTestSettings();
+
+    auto userScript = adoptNS([[WKUserScript alloc] initWithSource:@"var img = document.createElement('img'); img.src = 'in-app-browser:///should-load-for-main-frame-only'; document.getElementById('body').appendChild(img);" injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO]);
+
+    WKWebViewConfiguration *configuration = [WKWebViewConfiguration _test_configurationWithTestPlugInClassName:@"WebProcessPlugInWithInternals" configureJSCForTesting:YES];
+    auto schemeHandler = adoptNS([[InAppBrowserSchemeHandler alloc] init]);
+    [configuration setURLSchemeHandler:schemeHandler.get() forURLScheme:@"in-app-browser"];
+    [[configuration preferences] _setNeedsInAppBrowserPrivacyQuirks:NO];
+    [configuration setLimitsNavigationsToAppBoundDomains:YES];
+    [[configuration userContentController] addUserScript:userScript.get()];
+
+    auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:CGRectZero configuration:configuration]);
+    auto delegate = adoptNS([AppBoundDomainDelegate new]);
+    [webView setNavigationDelegate:delegate.get()];
+
+    // Load an app-bound domain with an iframe that is not app-bound.
+    NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"in-app-browser://apple.com/in-app-browser-privacy-test-user-script-nested-iframe"]];
+    [webView loadRequest:request];
+    [delegate waitForDidFinishNavigation];
+
+    EXPECT_TRUE(mainFrameReceivedScriptSource);
+    EXPECT_FALSE(subFrameReceivedScriptSource);
+    cleanUpInAppBrowserPrivacyTestSettings();
+}
+
+static NSMutableSet<WKFrameInfo *> *allFrames;
+
+TEST(InAppBrowserPrivacy, JavaScriptInNonAppBoundFrameFails)
+{
+    allFrames = [[NSMutableSet<WKFrameInfo *> alloc] init];
+
+    isDone = false;
+    initializeInAppBrowserPrivacyTestSettings();
+
+    auto userScript = adoptNS([[WKUserScript alloc] initWithSource:@"var img = document.createElement('img'); img.src = 'in-app-browser:///should-load-for-main-frame-only'; document.getElementById('body').appendChild(img);" injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO]);
+
+    WKWebViewConfiguration *configuration = [WKWebViewConfiguration _test_configurationWithTestPlugInClassName:@"WebProcessPlugInWithInternals" configureJSCForTesting:YES];
+    auto schemeHandler = adoptNS([[InAppBrowserSchemeHandler alloc] init]);
+    [configuration setURLSchemeHandler:schemeHandler.get() forURLScheme:@"in-app-browser"];
+    [[configuration preferences] _setNeedsInAppBrowserPrivacyQuirks:NO];
+    [configuration setLimitsNavigationsToAppBoundDomains:YES];
+    [[configuration userContentController] addUserScript:userScript.get()];
+
+    auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:CGRectZero configuration:configuration]);
+    auto navigationDelegate = adoptNS([[TestNavigationDelegate alloc] init]);
+
+    __block bool didFinishNavigation = false;
+    [navigationDelegate setDidFinishNavigation:^(WKWebView *, WKNavigation *) {
+        didFinishNavigation = true;
+    }];
+
+    [navigationDelegate setDecidePolicyForNavigationAction:[&] (WKNavigationAction *action, void (^decisionHandler)(WKNavigationActionPolicy)) {
+        if (action.targetFrame)
+            [allFrames addObject:action.targetFrame];
+
+        decisionHandler(WKNavigationActionPolicyAllow);
+    }];
+
+    [webView setNavigationDelegate:navigationDelegate.get()];
+    
+    // Load an app-bound domain with an iframe that is not app-bound.
+    NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"in-app-browser://apple.com/in-app-browser-privacy-test-user-script-nested-iframe"]];
+    [webView loadRequest:request];
+
+    TestWebKitAPI::Util::run(&didFinishNavigation);
+
+    EXPECT_EQ(allFrames.count, 3u);
+
+    static size_t finishedFrames = 0;
+    static bool isDone = false;
+
+    for (WKFrameInfo *frame in allFrames) {
+        bool isMainFrame = frame.isMainFrame;
+        [webView callAsyncJavaScript:@"return location.href;" arguments:nil inFrame:frame inContentWorld:WKContentWorld.defaultClientWorld completionHandler:[isMainFrame] (id result, NSError *error) {
+            if (isMainFrame) {
+                EXPECT_TRUE([result isKindOfClass:[NSString class]]);
+                EXPECT_FALSE(!!error);
+                EXPECT_TRUE([result isEqualToString:@"in-app-browser://apple.com/in-app-browser-privacy-test-user-script-nested-iframe"]);
+            } else {
+                EXPECT_TRUE(!!error);
+                EXPECT_EQ(error.code, WKErrorJavaScriptAppBoundDomain);
+            }
+
+            if (++finishedFrames == allFrames.count * 2)
+                isDone = true;
+        }];
+
+
+        [webView evaluateJavaScript:@"location.href;" inFrame:frame inContentWorld:WKContentWorld.defaultClientWorld completionHandler:[isMainFrame] (id result, NSError *error) {
+            if (isMainFrame) {
+                EXPECT_TRUE([result isKindOfClass:[NSString class]]);
+                EXPECT_FALSE(!!error);
+                EXPECT_TRUE([result isEqualToString:@"in-app-browser://apple.com/in-app-browser-privacy-test-user-script-nested-iframe"]);
+            } else {
+                EXPECT_TRUE(!!error);
+                EXPECT_EQ(error.code, WKErrorJavaScriptAppBoundDomain);
+            }
+
+            if (++finishedFrames == allFrames.count * 2)
+                isDone = true;
+        }];
+    }
+    
+    TestWebKitAPI::Util::run(&isDone);
+}
+
 TEST(InAppBrowserPrivacy, WebViewCategory)
 {
     initializeInAppBrowserPrivacyTestSettings();