| // Copyright 2021 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "content/browser/renderer_host/isolated_web_app_throttle.h" |
| |
| #include "base/feature_list.h" |
| #include "content/browser/renderer_host/frame_tree_node.h" |
| #include "content/browser/renderer_host/navigation_request.h" |
| #include "content/browser/web_exposed_isolation_info.h" |
| #include "content/common/navigation_params_utils.h" |
| #include "content/public/browser/content_browser_client.h" |
| #include "content/public/browser/isolated_web_apps_policy.h" |
| #include "content/public/browser/navigation_entry.h" |
| #include "content/public/browser/navigation_handle.h" |
| #include "content/public/browser/render_frame_host.h" |
| #include "content/public/browser/site_isolation_policy.h" |
| #include "content/public/browser/web_contents_user_data.h" |
| #include "content/public/common/content_client.h" |
| #include "content/public/common/content_features.h" |
| #include "content/public/common/page_type.h" |
| #include "url/origin.h" |
| #include "url/scheme_host_port.h" |
| |
| namespace content { |
| |
| namespace { |
| |
| // Stores the origin of the Isolated Web App that a WebContents is bound to. |
| // Every WebContents will have an instance of this class assigned to it during |
| // its first navigation, which will determine whether the WebContents hosts an |
| // Isolated Web App for its lifetime. Note that activating an alternative frame |
| // tree (e.g. preloading or portals) will NOT override this state. |
| class WebContentsIsolationInfo |
| : public WebContentsUserData<WebContentsIsolationInfo> { |
| public: |
| ~WebContentsIsolationInfo() override = default; |
| |
| bool is_isolated_application() { return isolated_origin_.has_value(); } |
| |
| const url::Origin& origin() { |
| DCHECK(is_isolated_application()); |
| return isolated_origin_.value(); |
| } |
| |
| private: |
| friend class WebContentsUserData<WebContentsIsolationInfo>; |
| explicit WebContentsIsolationInfo(WebContents* web_contents, |
| absl::optional<url::Origin> isolated_origin) |
| : WebContentsUserData<WebContentsIsolationInfo>(*web_contents), |
| isolated_origin_(isolated_origin) {} |
| |
| absl::optional<url::Origin> isolated_origin_; |
| |
| WEB_CONTENTS_USER_DATA_KEY_DECL(); |
| }; |
| WEB_CONTENTS_USER_DATA_KEY_IMPL(WebContentsIsolationInfo); |
| |
| absl::optional<url::SchemeHostPort> GetTupleFromOptionalOrigin( |
| const absl::optional<url::Origin>& origin) { |
| if (origin.has_value()) { |
| return origin->GetTupleOrPrecursorTupleIfOpaque(); |
| } |
| return absl::nullopt; |
| } |
| |
| } // namespace |
| |
| // static |
| std::unique_ptr<IsolatedWebAppThrottle> |
| IsolatedWebAppThrottle::MaybeCreateThrottleFor(NavigationHandle* handle) { |
| BrowserContext* browser_context = NavigationRequest::From(handle) |
| ->frame_tree_node() |
| ->navigator() |
| .controller() |
| .GetBrowserContext(); |
| |
| if (IsolatedWebAppsPolicy::AreIsolatedWebAppsEnabled(browser_context)) { |
| return std::make_unique<IsolatedWebAppThrottle>(handle); |
| } |
| return nullptr; |
| } |
| |
| IsolatedWebAppThrottle::IsolatedWebAppThrottle( |
| NavigationHandle* navigation_handle) |
| : NavigationThrottle(navigation_handle) {} |
| |
| IsolatedWebAppThrottle::~IsolatedWebAppThrottle() = default; |
| |
| NavigationThrottle::ThrottleCheckResult |
| IsolatedWebAppThrottle::WillStartRequest() { |
| bool requests_app_isolation = embedder_requests_app_isolation(); |
| auto* navigation_request = NavigationRequest::From(navigation_handle()); |
| dest_origin_ = navigation_request->GetTentativeOriginAtRequestTime(); |
| |
| // If this is the first navigation in this WebContents, save the isolation |
| // state to validate future navigations. |
| auto* web_contents_isolation_info = WebContentsIsolationInfo::FromWebContents( |
| navigation_handle()->GetWebContents()); |
| if (!web_contents_isolation_info) { |
| WebContentsIsolationInfo::CreateForWebContents( |
| navigation_handle()->GetWebContents(), |
| requests_app_isolation ? absl::make_optional(dest_origin_) |
| : absl::nullopt); |
| } |
| |
| FrameTreeNode* frame_tree_node = navigation_request->frame_tree_node(); |
| if (!frame_tree_node->is_on_initial_empty_document()) { |
| prev_origin_ = frame_tree_node->current_origin(); |
| } |
| |
| return DoThrottle(requests_app_isolation, NavigationThrottle::BLOCK_REQUEST); |
| } |
| |
| NavigationThrottle::ThrottleCheckResult |
| IsolatedWebAppThrottle::WillRedirectRequest() { |
| // On redirects, the old destination origin becomes the new previous origin. |
| auto* navigation_request = NavigationRequest::From(navigation_handle()); |
| prev_origin_ = dest_origin_; |
| dest_origin_ = navigation_request->GetTentativeOriginAtRequestTime(); |
| |
| return DoThrottle(embedder_requests_app_isolation(), |
| NavigationThrottle::BLOCK_REQUEST); |
| } |
| |
| NavigationThrottle::ThrottleCheckResult |
| IsolatedWebAppThrottle::WillProcessResponse() { |
| auto* navigation_request = NavigationRequest::From(navigation_handle()); |
| auto* assigned_rfh = static_cast<RenderFrameHostImpl*>( |
| navigation_request->GetRenderFrameHost()); |
| // Allow downloads and 204s (for these GetOriginToCommit returns nullopt). |
| if (!assigned_rfh) { |
| return NavigationThrottle::PROCEED; |
| } |
| |
| // Update |dest_origin_| to point to the final origin, which may have changed |
| // since the last WillStartRequest/WillRedirectRequest call. |
| dest_origin_ = navigation_request->GetOriginToCommit().value(); |
| const WebExposedIsolationInfo& assigned_isolation_info = |
| assigned_rfh->GetSiteInstance()->GetWebExposedIsolationInfo(); |
| return DoThrottle(assigned_isolation_info.is_isolated_application(), |
| NavigationThrottle::BLOCK_RESPONSE); |
| } |
| |
| bool IsolatedWebAppThrottle::OpenUrlExternal(const GURL& url) { |
| NavigationRequest* navigation_request = |
| NavigationRequest::From(navigation_handle()); |
| const FrameTreeNode* frame_tree_node = navigation_request->frame_tree_node(); |
| mojo::PendingRemote<network::mojom::URLLoaderFactory> loader_factory; |
| return GetContentClient()->browser()->HandleExternalProtocol( |
| url, |
| base::BindRepeating( |
| [](const int frame_tree_node_id) { |
| return WebContents::FromFrameTreeNodeId(frame_tree_node_id); |
| }, |
| frame_tree_node->frame_tree_node_id()), |
| frame_tree_node->frame_tree_node_id(), |
| navigation_request->GetNavigationUIData(), |
| /*is_primary_main_frame=*/true, /*is_in_fenced_frame_tree=*/false, |
| network::mojom::WebSandboxFlags::kNone, |
| (navigation_handle()->GetRedirectChain().size() > 1) |
| ? ui::PageTransition::PAGE_TRANSITION_SERVER_REDIRECT |
| : ui::PageTransition::PAGE_TRANSITION_LINK, |
| navigation_request->HasUserGesture(), |
| /*initiating_origin=*/absl::nullopt, |
| /*initiator_document=*/nullptr, &loader_factory); |
| } |
| |
| NavigationThrottle::ThrottleCheckResult IsolatedWebAppThrottle::DoThrottle( |
| bool needs_app_isolation, |
| NavigationThrottle::ThrottleAction block_action) { |
| auto* web_contents_isolation_info = WebContentsIsolationInfo::FromWebContents( |
| navigation_handle()->GetWebContents()); |
| DCHECK(web_contents_isolation_info); |
| |
| // Block navigations into Isolated Web Apps (IWA) from non-IWA contexts. |
| if (!web_contents_isolation_info->is_isolated_application()) { |
| return needs_app_isolation ? block_action : NavigationThrottle::PROCEED; |
| } |
| |
| // We want the following origin checks to be a bit more permissive than |
| // usual. In particular, if the isolation, previous, or destination origins |
| // are opaque, we want to use their precursor tuple for "origin" comparisons. |
| // This lets us allow navigations to/from data, error, or web bundle URLs |
| // that originate from the same precursor URLs. Other rules may block these |
| // navigations, but for the purpose of this throttle, these navigations are |
| // valid. |
| const url::SchemeHostPort& web_contents_isolation_tuple = |
| web_contents_isolation_info->origin().GetTupleOrPrecursorTupleIfOpaque(); |
| const url::SchemeHostPort& dest_tuple = |
| dest_origin_.GetTupleOrPrecursorTupleIfOpaque(); |
| DCHECK(web_contents_isolation_tuple.IsValid()); |
| |
| // If the main frame tries to leave the app's origin, cancel the |
| // navigation and open the URL in the systems' default application. |
| // Iframes are allowed to leave the app's origin. |
| if (dest_tuple != web_contents_isolation_tuple) { |
| if (navigation_handle()->IsInMainFrame()) { |
| OpenUrlExternal(navigation_handle()->GetURL()); |
| return NavigationThrottle::CANCEL; |
| } |
| return NavigationThrottle::PROCEED; |
| } |
| |
| // Block renderer-initiated iframe navigations into the app that were |
| // initiated by a non-app frame. This ensures that all iframe navigations into |
| // the app come from the app itself. |
| absl::optional<url::SchemeHostPort> prev_tuple = |
| GetTupleFromOptionalOrigin(prev_origin_); |
| if (prev_tuple.has_value() && |
| prev_tuple.value() != web_contents_isolation_tuple && |
| navigation_handle()->IsRendererInitiated()) { |
| // Main frames shouldn't have been allowed to leave the app's origin. |
| CHECK(!navigation_handle()->IsInMainFrame()); |
| |
| // Allow the navigation if it was initiated by the app, meaning it has a |
| // trusted destination URL. This only applies to the initial request, as |
| // redirect locations come from outside the app. |
| if (navigation_handle()->GetRedirectChain().size() == 1 && |
| navigation_handle()->GetInitiatorOrigin().has_value()) { |
| const url::SchemeHostPort& initiator_tuple = |
| navigation_handle() |
| ->GetInitiatorOrigin() |
| .value() |
| .GetTupleOrPrecursorTupleIfOpaque(); |
| if (initiator_tuple == web_contents_isolation_tuple) { |
| return NavigationThrottle::PROCEED; |
| } |
| } |
| return block_action; |
| } |
| |
| if (!navigation_handle()->IsInMainFrame()) { |
| { |
| // Block iframe navigations to the app's origin if the parent frame |
| // doesn't belong to the app. This prevents non-app frames from having |
| // access to an app frame. |
| const url::SchemeHostPort& parent_tuple = |
| navigation_handle() |
| ->GetParentFrame() |
| ->GetLastCommittedOrigin() |
| .GetTupleOrPrecursorTupleIfOpaque(); |
| if (parent_tuple != web_contents_isolation_tuple) { |
| return block_action; |
| } |
| } |
| |
| // Allow iframe same-origin navigations to blob: and data: URLs |
| // (cross-origin iframe navigation are already allowed and handled further |
| // up as part of the `dest_tuple != web_contents_isolation_tuple` |
| // condition). |
| if (navigation_handle()->GetURL().SchemeIs(url::kDataScheme) || |
| navigation_handle()->GetURL().SchemeIsBlob()) { |
| return NavigationThrottle::PROCEED; |
| } |
| } |
| |
| // At this point we know the navigation is same-tuple within an Isolated Web |
| // App. If the new page isn't isolated, block the navigation. |
| if (!needs_app_isolation) { |
| return block_action; |
| } |
| |
| return NavigationThrottle::PROCEED; |
| } |
| |
| bool IsolatedWebAppThrottle::embedder_requests_app_isolation() { |
| BrowserContext* browser_context = NavigationRequest::From(navigation_handle()) |
| ->frame_tree_node() |
| ->navigator() |
| .controller() |
| .GetBrowserContext(); |
| return SiteIsolationPolicy::ShouldUrlUseApplicationIsolationLevel( |
| browser_context, navigation_handle()->GetURL()); |
| } |
| |
| const char* IsolatedWebAppThrottle::GetNameForLogging() { |
| return "IsolatedWebAppThrottle"; |
| } |
| |
| } // namespace content |