// Copyright 2019 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/ipc_utils.h"

#include <optional>
#include <utility>

#include "base/debug/crash_logging.h"
#include "base/debug/dump_without_crashing.h"
#include "base/strings/string_util.h"
#include "base/strings/to_string.h"
#include "content/browser/bad_message.h"
#include "content/browser/blob_storage/chrome_blob_storage_context.h"
#include "content/browser/renderer_host/frame_tree_node.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/browser/security/cpsp/child_process_security_policy_impl.h"
#include "content/common/features.h"
#include "content/common/frame.mojom.h"
#include "content/common/navigation_params_utils.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/child_process_host.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/common/url_constants.h"
#include "mojo/public/cpp/system/message_pipe.h"
#include "net/http/http_request_headers.h"
#include "third_party/blink/public/mojom/navigation/navigation_params.mojom.h"
#include "ui/base/window_open_disposition.h"

namespace content {

namespace {

// Validates that the specified `disposition` could be legitimately sent by the
// renderer, as defined by NavigationPolicyToDisposition() in
// render_frame_impl.cc.
bool IsValidRendererDisposition(WindowOpenDisposition disposition) {
  switch (disposition) {
    case WindowOpenDisposition::CURRENT_TAB:
    case WindowOpenDisposition::NEW_FOREGROUND_TAB:
    case WindowOpenDisposition::NEW_BACKGROUND_TAB:
    case WindowOpenDisposition::NEW_POPUP:
    case WindowOpenDisposition::NEW_WINDOW:
    case WindowOpenDisposition::SAVE_TO_DISK:
    case WindowOpenDisposition::NEW_PICTURE_IN_PICTURE:
    case WindowOpenDisposition::NEW_SPLIT_VIEW:
      return true;
    default:
      // Certain dispositions, such as SWITCH_TO_TAB, are only used internally
      // within the browser process and should not be triggerable by a renderer
      // process. Allowing a compromised renderer to send those could let it
      // manipulate other tabs in unintended ways. See
      // https://crbug.com/486761170.
      return false;
  }
}

// Validates that |received_token| is non-null iff associated with a blob: URL.
bool VerifyBlobToken(
    ChildProcessId process_id,
    const mojo::PendingRemote<blink::mojom::BlobURLToken>& received_token,
    const GURL& received_url) {
  CHECK(process_id, base::NotFatalUntil::M154);

  if (received_token.is_valid()) {
    if (!received_url.SchemeIsBlob()) {
      bad_message::ReceivedBadMessage(
          process_id, bad_message::BLOB_URL_TOKEN_FOR_NON_BLOB_URL);
      return false;
    }
  }

  return true;
}

bool VerifyInitiatorOrigin(
    ChildProcessId process_id,
    const url::Origin& initiator_origin,
    const RenderFrameHostImpl* current_rfh = nullptr,
    GURL* navigation_url = nullptr,
    std::optional<blink::LocalFrameToken>* initiator_frame_token = nullptr) {
  // Important Note about opaque origins: these checks used to be skipped for
  // opaque origins in two tricky cases, error pages and MHTML subframes. These
  // exemptions are no longer needed now that ChildProcessSecurityPolicy's
  // enforcements have been switched to use committed origin tracking. Any error
  // page or MHTML subframe that could legitimately initiate a navigation has
  // already committed in this process, so its (opaque) origin has been recorded
  // by ChildProcessSecurityPolicyImpl::AddCommittedOrigin and the HostsOrigin()
  // check below will accept it, even if the precursor doesn't match the process
  // lock. This is covered in tests such as ErrorPageNavigationReload,
  // ErrorPageNavigationReload_InSubframe_BlockedByClient, and
  // NavigationMhtmlBrowserTest.DataIframe.
  //
  // Warning: avoid skipping this check for future cases, as doing so carries
  // security consequences, allowing the renderer to claim an opaque initiator
  // with an arbitrary precursor. See crbug.com/516398679 and
  // crbug.com/517606780.

  auto* policy = ChildProcessSecurityPolicyImpl::GetInstance();
  // TODO(crbug.com/379869738): Remove GetUnsafeValue.
  if (!policy->HostsOrigin(process_id.GetUnsafeValue(), initiator_origin)) {
    if (navigation_url) {
      static auto* const navigation_url_key =
          base::debug::AllocateCrashKeyString(
              "navigation_url", base::debug::CrashKeySize::Size256);
      base::debug::SetCrashKeyString(
          navigation_url_key,
          navigation_url->DeprecatedGetOriginAsURL().spec());
    }
    if (initiator_frame_token && initiator_frame_token->has_value()) {
      if (RenderFrameHostImpl* initiator_render_frame_host =
              RenderFrameHostImpl::FromFrameToken(
                  process_id, initiator_frame_token->value())) {
        static auto* const initiator_rfh_origin_key =
            base::debug::AllocateCrashKeyString(
                "initiator_rfh_origin", base::debug::CrashKeySize::Size256);
        base::debug::SetCrashKeyString(
            initiator_rfh_origin_key,
            initiator_render_frame_host->GetLastCommittedOrigin()
                .GetDebugString());
      }
    }

    if (current_rfh) {
      auto bool_to_crash_key = [](bool b) { return base::ToString(b); };
      static auto* const is_main_frame_key =
          base::debug::AllocateCrashKeyString(
              "is_main_frame", base::debug::CrashKeySize::Size32);
      base::debug::SetCrashKeyString(
          is_main_frame_key, bool_to_crash_key(current_rfh->is_main_frame()));

      static auto* const is_outermost_frame_key =
          base::debug::AllocateCrashKeyString(
              "is_outermost_frame", base::debug::CrashKeySize::Size32);
      base::debug::SetCrashKeyString(
          is_outermost_frame_key,
          bool_to_crash_key(current_rfh->IsOutermostMainFrame()));

      static auto* const is_on_initial_empty_document_key =
          base::debug::AllocateCrashKeyString(
              "is_on_initial_empty_doc", base::debug::CrashKeySize::Size32);
      base::debug::SetCrashKeyString(
          is_on_initial_empty_document_key,
          bool_to_crash_key(
              current_rfh->frame_tree_node()->is_on_initial_empty_document()));

      static auto* const last_committed_origin_key =
          base::debug::AllocateCrashKeyString(
              "last_committed_origin", base::debug::CrashKeySize::Size256);
      base::debug::SetCrashKeyString(
          last_committed_origin_key,
          current_rfh->GetLastCommittedOrigin().GetDebugString());

      if (current_rfh->GetParentOrOuterDocumentOrEmbedder()) {
        static auto* const parent_etc_origin_key =
            base::debug::AllocateCrashKeyString(
                "parent_etc_origin", base::debug::CrashKeySize::Size256);
        base::debug::SetCrashKeyString(
            parent_etc_origin_key,
            current_rfh->GetParentOrOuterDocumentOrEmbedder()
                ->GetLastCommittedOrigin()
                .GetDebugString());
      }

      if (FrameTreeNode* opener = current_rfh->frame_tree_node()->opener()) {
        static auto* const opener_origin_key =
            base::debug::AllocateCrashKeyString(
                "opener_origin", base::debug::CrashKeySize::Size256);
        base::debug::SetCrashKeyString(opener_origin_key,
                                       opener->current_frame_host()
                                           ->GetLastCommittedOrigin()
                                           .GetDebugString());
      }
    }

    bad_message::ReceivedBadMessage(process_id,
                                    bad_message::INVALID_INITIATOR_ORIGIN);
    return false;
  }

  return true;
}

}  // namespace

bool VerifyDownloadUrlParams(RenderProcessHost* process,
                             const blink::mojom::DownloadURLParams& params) {
  CHECK_CURRENTLY_ON(BrowserThread::UI, base::NotFatalUntil::M154);
  CHECK(process);
  ChildProcessId process_id = process->GetID();

  // Verifies |params.blob_url_token| is appropriately set.
  if (!VerifyBlobToken(process_id, params.blob_url_token, params.url))
    return false;

  // Verify |params.initiator_origin|.
  if (params.initiator_origin &&
      !VerifyInitiatorOrigin(process_id, *params.initiator_origin))
    return false;

  // Verify |params.referrer|.
  if (params.referrer && !params.referrer->url.is_empty()) {
    auto* policy = ChildProcessSecurityPolicyImpl::GetInstance();
    if (!policy->HostsOrigin(process_id.GetUnsafeValue(),
                             url::Origin::Create(params.referrer->url))) {
      bad_message::ReceivedBadMessage(
          process_id, bad_message::RFH_DOWNLOAD_URL_INVALID_REFERRER);
      return false;
    }
  }

  // If |params.url| is not set, this must be a large data URL being passed
  // through |params.data_url_blob|.
  if (!params.url.is_valid() && !params.data_url_blob.is_valid())
    return false;

  // Verification succeeded.
  return true;
}

bool VerifyOpenURLParams(RenderFrameHostImpl* current_rfh,
                         RenderProcessHost* process,
                         const blink::mojom::OpenURLParamsPtr& params,
                         GURL* out_validated_url,
                         scoped_refptr<network::SharedURLLoaderFactory>*
                             out_blob_url_loader_factory) {
  CHECK_CURRENTLY_ON(BrowserThread::UI, base::NotFatalUntil::M154);
  CHECK(current_rfh, base::NotFatalUntil::M154);
  CHECK(process, base::NotFatalUntil::M154);
  CHECK(out_validated_url, base::NotFatalUntil::M154);
  CHECK(out_blob_url_loader_factory, base::NotFatalUntil::M154);
  ChildProcessId process_id = process->GetID();

  // Verify |params.url| and populate |out_validated_url|.
  *out_validated_url = params->url;
  process->FilterURL(false, out_validated_url);

  // Verify |params.blob_url_token| and populate |out_blob_url_loader_factory|.
  if (!VerifyBlobToken(process_id, params->blob_url_token, params->url))
    return false;

  if (params->blob_url_token.is_valid()) {
    *out_blob_url_loader_factory =
        ChromeBlobStorageContext::URLLoaderFactoryForToken(
            process->GetStoragePartition(), std::move(params->blob_url_token));
  }

  // Verify |params.post_body|.
  auto* policy = ChildProcessSecurityPolicyImpl::GetInstance();
  if (!policy->CanReadRequestBody(process, params->post_body)) {
    bad_message::ReceivedBadMessage(process,
                                    bad_message::ILLEGAL_UPLOAD_PARAMS);
    return false;
  }

  // Verify |params.initiator_origin|.
  if (!VerifyInitiatorOrigin(process_id, params->initiator_origin, current_rfh,
                             &params->url, &params->initiator_frame_token)) {
    return false;
  }

  if (!VerifyNavigationHeaders(process, params->extra_headers)) {
    return false;
  }

  if (params->initiator_base_url) {
    // `initiator_base_url` should only be defined for about:blank and
    // about:srcdoc navigations, and should never be an empty GURL (if it is not
    // nullopt).
    if (params->initiator_base_url->is_empty() ||
        !(out_validated_url->IsAboutBlank() ||
          out_validated_url->IsAboutSrcdoc())) {
      return false;
    }
  }

  // Verify that the initiator frame can navigate `current_rfh`.
  if (!VerifyNavigationInitiator(current_rfh, params->initiator_frame_token,
                                 process_id)) {
    return false;
  }

  if (params->is_container_initiated) {
    if (!current_rfh->GetParent() ||
        (current_rfh->GetParent()->GetFrameToken() !=
         params->initiator_frame_token)) {
      mojo::ReportBadMessage(
          "container initiated navigation from non-parent process");
      return false;
    }
  }

  // Certain dispositions should never be sent from the renderer, so terminate
  // the renderer process if an unexpected disposition is encountered.
  if (!IsValidRendererDisposition(params->disposition)) {
    bad_message::ReceivedBadMessage(
        process, bad_message::RFH_OPEN_URL_INVALID_DISPOSITION);
    return false;
  }

  // Verification succeeded.
  return true;
}

bool VerifyBeginNavigationCommonParams(
    const RenderFrameHostImpl& current_rfh,
    blink::mojom::CommonNavigationParams* common_params,
    std::optional<blink::LocalFrameToken>& initiator_frame_token) {
  CHECK_CURRENTLY_ON(BrowserThread::UI, base::NotFatalUntil::M154);
  CHECK(common_params, base::NotFatalUntil::M154);
  RenderProcessHost* process = current_rfh.GetProcess();
  ChildProcessId process_id = process->GetID();

  // Verify (and possibly rewrite) |url|.
  process->FilterURL(false, &common_params->url);
  if (common_params->url.SchemeIs(kChromeErrorScheme)) {
    mojo::ReportBadMessage("Renderer cannot request error page URLs directly");
    return false;
  }

  // Verify |post_data|.
  auto* policy = ChildProcessSecurityPolicyImpl::GetInstance();
  if (!policy->CanReadRequestBody(process, common_params->post_data)) {
    bad_message::ReceivedBadMessage(process,
                                    bad_message::ILLEGAL_UPLOAD_PARAMS);
    return false;
  }

  // Verify |transition| is webby.
  if (!PageTransitionIsWebTriggerable(
          ui::PageTransitionFromInt(common_params->transition))) {
    bad_message::ReceivedBadMessage(
        process, bad_message::RFHI_BEGIN_NAVIGATION_NON_WEBBY_TRANSITION);
    return false;
  }

  // Verify |initiator_origin|.
  if (!common_params->initiator_origin.has_value()) {
    bad_message::ReceivedBadMessage(
        process, bad_message::RFHI_BEGIN_NAVIGATION_MISSING_INITIATOR_ORIGIN);
    return false;
  }
  if (!VerifyInitiatorOrigin(
          process_id, common_params->initiator_origin.value(), &current_rfh,
          &common_params->url, &initiator_frame_token)) {
    return false;
  }

  // Verify |base_url_for_data_url|.
  if (!common_params->base_url_for_data_url.is_empty()) {
    // Kills the process. http://crbug.com/726142
    bad_message::ReceivedBadMessage(
        process, bad_message::RFH_BASE_URL_FOR_DATA_URL_SPECIFIED);
    return false;
  }

  // Verify |initiator_base_url|. The value is allowed to be nullopt, but if it
  // isn't then it's required to be non-empty (the renderer is supposed to
  // guarantee this). If this condition isn't met, CHECK in NavigationRequest's
  // constructor will fail.
  if (common_params->initiator_base_url &&
      common_params->initiator_base_url->is_empty()) {
    bad_message::ReceivedBadMessage(
        process, bad_message::RFH_INITIATOR_BASE_URL_IS_EMPTY);
    return false;
  }

  // Asynchronous (browser-controlled, but) renderer-initiated navigations can
  // not be same-document. Allowing this incorrectly could have us try to
  // navigate an existing document to a different site.
  if (NavigationTypeUtils::IsSameDocument(common_params->navigation_type))
    return false;

  // Verification succeeded.
  return true;
}

bool VerifyClientSideRedirectUrl(const RenderFrameHostImpl& current_rfh,
                                 GURL* client_side_redirect_url) {
  CHECK_CURRENTLY_ON(BrowserThread::UI);
  CHECK(client_side_redirect_url);

  // `client_side_redirect_url` is only populated if the navigation's transition
  // type is a client side redirect. For all other renderer-initiated
  // navigations, it is intentionally empty.
  if (client_side_redirect_url->is_empty()) {
    return true;
  }

  RenderProcessHost* process = current_rfh.GetProcess();
  CHECK(process);

  process->FilterURL(false, client_side_redirect_url);

  // Verify that `process` has hosted `redirect_origin` either as a standard
  // tuple origin or as the precursor of an opaque origin (e.g. when the
  // redirect is initiated by a sandboxed document). URLs blocked by FilterURL()
  // are rewritten to about:blank#blocked, which is treated as the
  // `current_rfh`'s origin.
  url::Origin redirect_origin = url::Origin::Resolve(
      *client_side_redirect_url, current_rfh.GetLastCommittedOrigin());
  auto* policy = ChildProcessSecurityPolicyImpl::GetInstance();
  ChildProcessId process_id = process->GetID();
  if (!policy->HostsOrigin(process_id.GetUnsafeValue(), redirect_origin) &&
      !policy->HostsOrigin(process_id.GetUnsafeValue(),
                           redirect_origin.DeriveNewOpaqueOrigin())) {
    bad_message::ReceivedBadMessage(
        process, bad_message::RFHI_INVALID_CLIENT_SIDE_REDIRECT_URL);
    return false;
  }

  return true;
}

bool VerifyCreateNewWindowParams(const RenderFrameHostImpl& current_rfh,
                                 const mojom::CreateNewWindowParams& params) {
  CHECK_CURRENTLY_ON(BrowserThread::UI, base::NotFatalUntil::M154);
  RenderProcessHost* process = current_rfh.GetProcess();

  // Certain dispositions should never be sent from the renderer, so terminate
  // the renderer process if an unexpected disposition is encountered.
  if (!IsValidRendererDisposition(params.disposition)) {
    bad_message::ReceivedBadMessage(
        process, bad_message::RFH_CREATE_NEW_WINDOW_INVALID_DISPOSITION);
    return false;
  }

  if (params.pip_options &&
      params.disposition != WindowOpenDisposition::NEW_PICTURE_IN_PICTURE) {
    bad_message::ReceivedBadMessage(
        process, bad_message::RFH_CREATE_NEW_WINDOW_INVALID_PIP_OPTIONS);
    return false;
  }

  // Verify `form_submission_post_data`.
  auto* policy = ChildProcessSecurityPolicyImpl::GetInstance();
  if (!policy->CanReadRequestBody(process, params.form_submission_post_data)) {
    bad_message::ReceivedBadMessage(process,
                                    bad_message::ILLEGAL_UPLOAD_PARAMS);
    return false;
  }

  return true;
}

bool VerifyNavigationInitiator(
    RenderFrameHostImpl* current_rfh,
    const std::optional<blink::LocalFrameToken>& initiator_frame_token,
    ChildProcessId initiator_process_id) {
  // Verify that a frame inside a fenced frame cannot navigate its ancestors,
  // unless the frame being navigated is the outermost main frame.
  if (current_rfh->IsOutermostMainFrame())
    return true;

  if (!initiator_frame_token)
    return true;

  RenderFrameHostImpl* initiator_render_frame_host =
      RenderFrameHostImpl::FromFrameToken(initiator_process_id,
                                          initiator_frame_token.value());
  if (!initiator_render_frame_host)
    return true;

  // Verify that a frame cannot navigate a frame with a different fenced frame
  // nonce, unless the navigating frame is a fenced frame root and its owner
  // frame has the same fenced frame nonce as the initiator frame (e.g. in a
  // A(A1,A2(FF)) setup, A, A1, and A2 are all allowed to navigate FF).
  std::optional<base::UnguessableToken> initiator_fenced_frame_nonce =
      initiator_render_frame_host->frame_tree_node()->GetFencedFrameNonce();
  if (initiator_fenced_frame_nonce !=
      current_rfh->frame_tree_node()->GetFencedFrameNonce()) {
    if (!current_rfh->IsFencedFrameRoot() ||
        current_rfh->frame_tree_node()
                ->GetParentOrOuterDocument()
                ->frame_tree_node()
                ->GetFencedFrameNonce() != initiator_fenced_frame_nonce) {
      mojo::ReportBadMessage(
          "The fenced frame nonces of initiator and current frame don't match, "
          "nor is the current frame a fenced frame root whose owner frame has "
          "the same fenced frame nonce as the initiator frame.");
      return false;
    }
  }

  if (!initiator_render_frame_host->IsNestedWithinFencedFrame())
    return true;

  FrameTreeNode* node = initiator_render_frame_host->frame_tree_node();
  if (node == current_rfh->frame_tree_node())
    return true;

  while (node) {
    node = node->parent() ? node->parent()->frame_tree_node() : nullptr;

    if (node == current_rfh->frame_tree_node()) {
      mojo::ReportBadMessage(
          "A frame in a fenced frame tree cannot navigate an ancestor frame.");
      return false;
    }
  }

  return true;
}

bool VerifyNavigationHeaders(RenderProcessHost* process,
                             const std::string& headers) {
  // Navigation headers may be LF-separated and are normalized to CRLF
  // before being applied to the outgoing request.
  // AddHeadersFromString() splits only on CRLF, so apply the same normalization
  // here to ensure consistent header verification.
  std::string headers_crlf;
  base::ReplaceChars(headers, "\n", "\r\n", &headers_crlf);

  net::HttpRequestHeaders parsed_headers;
  parsed_headers.AddHeadersFromString(headers_crlf);
  for (net::HttpRequestHeaders::Iterator header(parsed_headers);
       header.GetNext();) {
    // Headers should be strictly allowlisted because there can be security
    // consequences if a compromised renderer can set arbitrary headers (e.g.,
    // for CSRF prevention).
    //
    // This list allowlists `Origin`, but the value of the `Origin` header is
    // further validated in NavigationRequest::AddAdditionalRequestHeaders.
    if (header.name() != net::HttpRequestHeaders::kUpgradeInsecureRequests &&
        header.name() != net::HttpRequestHeaders::kOrigin &&
        header.name() != net::HttpRequestHeaders::kContentType &&
        header.name() != net::HttpRequestHeaders::kUserAgent &&
        header.name() != net::HttpRequestHeaders::kSecPurpose &&
        header.name() != net::HttpRequestHeaders::kDNT &&
        header.name() != net::HttpRequestHeaders::kSecGPC) {
      if (base::FeatureList::IsEnabled(
              features::kKillOnInvalidNavigationHeaders)) {
        SCOPED_CRASH_KEY_STRING64("Bug487795397", "invalid_header",
                                  header.name());
        bad_message::ReceivedBadMessage(
            process, bad_message::RFH_INVALID_NAVIGATION_HEADERS);
        return false;
      }
    }
  }
  return true;
}

}  // namespace content
