// Copyright 2016 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/ancestor_throttle.h"

#include <algorithm>

#include "base/feature_list.h"
#include "base/memory/ptr_util.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "content/browser/renderer_host/frame_tree.h"
#include "content/browser/renderer_host/frame_tree_node.h"
#include "content/browser/renderer_host/navigation_request.h"
#include "content/browser/renderer_host/navigator.h"
#include "content/browser/renderer_host/render_frame_host_csp_context.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/browser/renderer_host/render_view_host_delegate.h"
#include "content/browser/renderer_host/render_view_host_impl.h"
#include "content/common/features.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/navigation_throttle.h"
#include "content/public/browser/storage_partition.h"
#include "content/public/common/content_client.h"
#include "net/http/http_response_headers.h"
#include "services/network/public/cpp/content_security_policy/content_security_policy.h"
#include "services/network/public/cpp/content_security_policy/csp_context.h"
#include "third_party/blink/public/mojom/devtools/console_message.mojom.h"
#include "third_party/blink/public/mojom/use_counter/metrics/web_feature.mojom.h"
#include "url/gurl.h"

namespace content {

namespace {

bool HeadersContainFrameAncestorsCSP(
    const network::mojom::ParsedHeadersPtr& headers) {
  return std::ranges::any_of(
      headers->content_security_policy, [](const auto& csp) {
        return csp->header->type ==
                   network::mojom::ContentSecurityPolicyType::kEnforce &&
               csp->directives.count(
                   network::mojom::CSPDirectiveName::FrameAncestors);
      });
}

// From a RenderFrameHost |frame|, return its parent. This escapes FencedFrames
// that allow for information inflow, but does not escape nested WebContents.
// This returns nullptr for the top-level document and fenced frame roots (if
// information inflow is not allowed for the fenced frame). |request| is only
// supplied when |frame| is the frame being navigated. The FencedFrameProperties
// has not been installed in |frame|'s FrameTreeNode yet. Instead, we look at
// the one attached to the NavigationRequest.
RenderFrameHostImpl* GetParentForFrameAncestors(NavigationRequest* request,
                                                RenderFrameHostImpl* frame) {
  bool allows_information_inflow = false;
  if (base::FeatureList::IsEnabled(
          blink::features::kFencedFramesLocalUnpartitionedDataAccess)) {
    if (request) {
      allows_information_inflow =
          !request->GetFencedFrameProperties().has_value() ||
          request->GetFencedFrameProperties()->allows_information_inflow();
    } else {
      // We are in one of the navigated frame's ancestors.
      allows_information_inflow =
          !frame->frame_tree_node()->HasFencedFrameProperties() ||
          frame->frame_tree_node()
              ->GetFencedFrameProperties()
              ->allows_information_inflow();
    }
  } else {
    allows_information_inflow = !frame->IsFencedFrameRoot();
  }

  if (!allows_information_inflow && request &&
      base::FeatureList::IsEnabled(
          blink::features::kFencedFramesLocalUnpartitionedDataAccess)) {
    request->AddDeferredConsoleMessage(
        blink::mojom::ConsoleMessageLevel::kWarning,
        "'CSP frame-ancestors' and 'X-Frame-Options' directives will not look "
        "past a fenced frame boundary if created with an API that disallows "
        "information inflow, such as Protected Audience.");
  }

  return allows_information_inflow ? frame->GetParentOrOuterDocument()
                                   : nullptr;
}

}  // namespace

// static
void AncestorThrottle::CreateAndAdd(NavigationThrottleRegistry& registry) {
  CHECK_CURRENTLY_ON(BrowserThread::UI, base::NotFatalUntil::M152);

  registry.AddThrottle(base::WrapUnique(new AncestorThrottle(registry)));
}

AncestorThrottle::~AncestorThrottle() {}

NavigationThrottle::ThrottleCheckResult
AncestorThrottle::WillRedirectRequest() {
  // During a redirect, we don't know which RenderFrameHost we'll end up in,
  // so we can't log reliably to the console. We should be able to work around
  // this iff we decide to ship the redirect-blocking behavior, but for now
  // we'll just skip the console-logging bits to collect metrics.
  NavigationThrottle::ThrottleCheckResult result = ProcessResponseImpl(
      LoggingDisposition::DO_NOT_LOG_TO_CONSOLE, false /* is_response_check */);

  // TODO(mkwst): We need to decide whether we'll be able to get away with
  // tightening the XFO check to include redirect responses once we have a
  // feel for the REDIRECT_WOULD_BE_BLOCKED numbers we're collecting above.
  // Until then, we'll allow the response to proceed: https://crbug.com/835465.
  return NavigationThrottle::PROCEED;
}

NavigationThrottle::ThrottleCheckResult
AncestorThrottle::WillProcessResponse() {
  return ProcessResponseImpl(LoggingDisposition::LOG_TO_CONSOLE,
                             true /* is_response_check */);
}

NavigationThrottle::ThrottleCheckResult AncestorThrottle::ProcessResponseImpl(
    LoggingDisposition logging,
    bool is_response_check) {
  NavigationRequest* request = NavigationRequest::From(navigation_handle());

  if (request->IsInOutermostMainFrame()) {
    // Allow main frame navigations.
    return NavigationThrottle::PROCEED;
  }

  // 204/205 responses and downloads are not sent to the renderer and don't need
  // to be checked.
  if (is_response_check && !request->response_should_be_rendered()) {
    return NavigationThrottle::PROCEED;
  }

  const std::vector<network::mojom::ContentSecurityPolicyPtr>&
      content_security_policies =
          request->response()->parsed_headers->content_security_policy;

  // CSP: frame-ancestors is checked only for the final response.
  if (is_response_check &&
      EvaluateFrameAncestors(content_security_policies) == CheckResult::BLOCK) {
    return NavigationThrottle::BLOCK_RESPONSE;
  }

  GURL url = navigation_handle()->GetURL();
  if (!is_response_check &&
      base::FeatureList::IsEnabled(
          features::kAncestorThrottleEvaluateRedirectSource)) {
    const std::vector<GURL>& redirect_chain =
        navigation_handle()->GetRedirectChain();
    // A redirect response implies that the redirect chain contains at least the
    // initial URL and the redirect target.
    CHECK_GE(redirect_chain.size(), 2u);
    // At the point where WillRedirectRequest is called, the navigation state
    // has already been updated to the redirect target URL. The response
    // headers, however, belong to the redirect source URL. We must use the
    // redirect source URL (the second to last element in the redirect chain)
    // to evaluate the response headers.
    url = redirect_chain[redirect_chain.size() - 2];
  }

  if (EvaluateXFrameOptions(logging, url) == CheckResult::BLOCK) {
    return NavigationThrottle::BLOCK_RESPONSE;
  }

  if (EvaluateEmbeddingOptIn(logging, url) == CheckResult::BLOCK) {
    return NavigationThrottle::BLOCK_RESPONSE;
  }

  return NavigationThrottle::PROCEED;
}

const char* AncestorThrottle::GetNameForLogging() {
  return "AncestorThrottle";
}

AncestorThrottle::AncestorThrottle(NavigationThrottleRegistry& registry)
    : NavigationThrottle(registry) {}

void AncestorThrottle::ParseXFrameOptionsError(
    const net::HttpResponseHeaders* headers,
    network::mojom::XFrameOptionsValue disposition,
    const GURL& url) {
  CHECK(disposition == network::mojom::XFrameOptionsValue::kConflict ||
            disposition == network::mojom::XFrameOptionsValue::kInvalid,
        base::NotFatalUntil::M152);
  CHECK(headers, base::NotFatalUntil::M152);

  std::string value =
      headers->GetNormalizedHeader("X-Frame-Options").value_or(std::string());

  std::string message;
  if (disposition == network::mojom::XFrameOptionsValue::kConflict) {
    message = base::StringPrintf(
        "Refused to display '%s' in a frame because it set multiple "
        "'X-Frame-Options' headers with conflicting values "
        "('%s'). Falling back to 'deny'.",
        url::Origin::Create(url).GetURL().spec().c_str(), value.c_str());
  } else {
    message = base::StringPrintf(
        "Invalid 'X-Frame-Options' header encountered when loading '%s': "
        "'%s' is not a recognized directive. The header will be ignored.",
        url::Origin::Create(url).GetURL().spec().c_str(), value.c_str());
  }

  AddMessageToConsole(blink::mojom::ConsoleMessageLevel::kError,
                      std::move(message));
}

void AncestorThrottle::ConsoleErrorEmbeddingRequiresOptIn(const GURL& url) {
  CHECK(base::FeatureList::IsEnabled(features::kEmbeddingRequiresOptIn),
        base::NotFatalUntil::M152);
  std::string message = base::StringPrintf(
      "Refused to display '%s' in a frame: It did not opt-into cross-origin "
      "embedding by setting either an 'X-Frame-Options' header, or a "
      "'Content-Security-Policy' header containing a 'frame-ancestors' "
      "directive.",
      url::Origin::Create(url).GetURL().spec().c_str());

  AddMessageToConsole(blink::mojom::ConsoleMessageLevel::kError,
                      std::move(message));
}

void AncestorThrottle::ConsoleErrorXFrameOptions(
    network::mojom::XFrameOptionsValue disposition,
    const GURL& url) {
  CHECK(disposition == network::mojom::XFrameOptionsValue::kDeny ||
            disposition == network::mojom::XFrameOptionsValue::kSameOrigin,
        base::NotFatalUntil::M152);
  std::string message = base::StringPrintf(
      "Refused to display '%s' in a frame because it set 'X-Frame-Options' "
      "to '%s'.",
      url::Origin::Create(url).GetURL().spec().c_str(),
      disposition == network::mojom::XFrameOptionsValue::kDeny ? "deny"
                                                               : "sameorigin");

  AddMessageToConsole(blink::mojom::ConsoleMessageLevel::kError,
                      std::move(message));
}

void AncestorThrottle::AddMessageToConsole(
    blink::mojom::ConsoleMessageLevel level,
    std::string message) {
  NavigationRequest::From(navigation_handle())
      ->AddDeferredConsoleMessage(level, std::move(message));
}

AncestorThrottle::CheckResult AncestorThrottle::EvaluateXFrameOptions(
    LoggingDisposition logging,
    const GURL& url) {
  NavigationRequest* request = NavigationRequest::From(navigation_handle());
  network::mojom::XFrameOptionsValue disposition =
      request->response()->parsed_headers->xfo;

  // If 'X-Frame-Options' would potentially block the response, check whether
  // the 'frame-ancestors' CSP directive should take effect instead. See
  // https://www.w3.org/TR/CSP/#frame-ancestors-and-frame-options
  if (disposition != network::mojom::XFrameOptionsValue::kNone &&
      disposition != network::mojom::XFrameOptionsValue::kAllowAll &&
      HeadersContainFrameAncestorsCSP(request->response()->parsed_headers)) {
    return CheckResult::PROCEED;
  }

  switch (disposition) {
    case network::mojom::XFrameOptionsValue::kConflict:
      if (logging == LoggingDisposition::LOG_TO_CONSOLE) {
        ParseXFrameOptionsError(request->GetResponseHeaders(), disposition,
                                url);
      }
      return CheckResult::BLOCK;

    case network::mojom::XFrameOptionsValue::kInvalid:
      if (logging == LoggingDisposition::LOG_TO_CONSOLE) {
        ParseXFrameOptionsError(request->GetResponseHeaders(), disposition,
                                url);
      }
      // TODO(mkwst): Consider failing here, especially if we end up shipping
      // a new default behavior which requires embedees to explicitly opt-in
      // to being embedded: https://crbug.com/1153274.
      return CheckResult::PROCEED;

    case network::mojom::XFrameOptionsValue::kDeny:
      if (logging == LoggingDisposition::LOG_TO_CONSOLE) {
        ConsoleErrorXFrameOptions(disposition, url);
      }
      return CheckResult::BLOCK;

    case network::mojom::XFrameOptionsValue::kSameOrigin: {
      // Block the request when any ancestor is not same-origin.
      // We enforce XFrameOptions in the outer documents, but not for
      // embedders/GuestViews.
      RenderFrameHostImpl* parent = GetParentForFrameAncestors(
          request, request->frame_tree_node()->current_frame_host());
      url::Origin current_origin = url::Origin::Create(url);
      while (parent) {
        if (!parent->GetLastCommittedOrigin().IsSameOriginWith(
                current_origin)) {
          if (logging == LoggingDisposition::LOG_TO_CONSOLE) {
            ConsoleErrorXFrameOptions(disposition, url);
          }
          return CheckResult::BLOCK;
        }
        parent = GetParentForFrameAncestors(nullptr, parent);
      }
      return CheckResult::PROCEED;
    }

    case network::mojom::XFrameOptionsValue::kNone:
      return CheckResult::PROCEED;
    case network::mojom::XFrameOptionsValue::kAllowAll:
      return CheckResult::PROCEED;
  }
}

AncestorThrottle::CheckResult AncestorThrottle::EvaluateEmbeddingOptIn(
    LoggingDisposition logging,
    const GURL& url) {
  // If the proposal in https://github.com/mikewest/embedding-requires-opt-in is
  // enabled, a response will be blocked unless it's explicitly opted-into
  // being embeddable via 'X-Frame-Options'/'frame-ancestors', or is same-origin
  // with its ancestors.
  // We enforce frame-ancestors in the outer documents, but not for
  // embedders/GuestViews.
  NavigationRequest* request = NavigationRequest::From(navigation_handle());
  if (request->response()->parsed_headers->xfo ==
          network::mojom::XFrameOptionsValue::kNone &&
      !HeadersContainFrameAncestorsCSP(request->response()->parsed_headers)) {
    RenderFrameHostImpl* parent = GetParentForFrameAncestors(
        request, request->frame_tree_node()->current_frame_host());
    while (parent) {
      if (!parent->GetLastCommittedOrigin().IsSameOriginWith(url)) {
        GetContentClient()->browser()->LogWebFeatureForCurrentPage(
            parent, blink::mojom::WebFeature::
                        kEmbeddedCrossOriginFrameWithoutFrameAncestorsOrXFO);

        if (!base::FeatureList::IsEnabled(features::kEmbeddingRequiresOptIn)) {
          return CheckResult::PROCEED;
        }

        if (logging == LoggingDisposition::LOG_TO_CONSOLE) {
          ConsoleErrorEmbeddingRequiresOptIn(url);
        }

        return CheckResult::BLOCK;
      }
      parent = GetParentForFrameAncestors(nullptr, parent);
    }
  }
  return CheckResult::PROCEED;
}

AncestorThrottle::CheckResult AncestorThrottle::EvaluateFrameAncestors(
    const std::vector<network::mojom::ContentSecurityPolicyPtr>&
        content_security_policy) {
  NavigationRequest* request = NavigationRequest::From(navigation_handle());
  // TODO(lfg): If the initiating document is known and correspond to the
  // navigating frame's current document, consider using:
  // navigation_request().common_params().source_location here instead.
  auto empty_source_location = network::mojom::SourceLocation::New();

  // Check CSP frame-ancestors against every parent. We enforce frame-ancestors
  // in the outer documents (except for fenced frames created under certain
  // conditions), but not for embedders or GuestViews.
  RenderFrameHostImpl* parent = GetParentForFrameAncestors(
      request, static_cast<RenderFrameHostImpl*>(
                   navigation_handle()->GetRenderFrameHost()));

  while (parent) {
    // CSP violations (if any) are reported via the disallowed ancestor of the
    // navigated frame (because while the throttle runs the navigation hasn't
    // committed yet and the target frame might not yet have a URLLoaderFactory
    // that could be used to report the violation).
    // See also https://crbug.com/1111049.
    network::CSPCheckResult result =
        RenderFrameHostCSPContext(parent).IsAllowedByCsp(
            content_security_policy,
            network::mojom::CSPDirectiveName::FrameAncestors,
            parent->GetLastCommittedOrigin().GetURL(),
            GURL(),  // url_before_redirects is ignored for frame-ancestors
            navigation_handle()->WasServerRedirect(), empty_source_location,
            network::CSPContext::CheckCSPDisposition::CHECK_ALL_CSP,
            /*is_opaque_fenced_frame=*/false);
    if (result.WouldBlockIfWildcardDoesNotMatchWs()) {
      GetContentClient()->browser()->LogWebFeatureForCurrentPage(
          parent,
          blink::mojom::WebFeature::kCspWouldBlockIfWildcardDoesNotMatchWs);
    }
    if (!result) {
      return CheckResult::BLOCK;
    }
    parent = GetParentForFrameAncestors(nullptr, parent);
  }

  return CheckResult::PROCEED;
}

}  // namespace content
