// Copyright 2013 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "extensions/common/manifest_handlers/csp_info.h"

#include <memory>
#include <string_view>
#include <utility>

#include "base/feature_list.h"
#include "base/no_destructor.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/values.h"
#include "extensions/common/constants.h"
#include "extensions/common/csp_validator.h"
#include "extensions/common/error_utils.h"
#include "extensions/common/extension_features.h"
#include "extensions/common/install_warning.h"
#include "extensions/common/manifest_constants.h"
#include "extensions/common/manifest_handlers/sandboxed_page_info.h"
#include "extensions/common/mojom/manifest.mojom-shared.h"

namespace extensions {

namespace keys = manifest_keys;
namespace errors = manifest_errors;

using csp_validator::ContentSecurityPolicyIsLegal;
using csp_validator::SanitizeContentSecurityPolicy;

namespace {

const char kDefaultContentSecurityPolicy[] =
    "script-src 'self' blob: filesystem:; "
    "object-src 'self' blob: filesystem:;";

const char kDefaultSandboxedPageContentSecurityPolicy[] =
    "sandbox allow-scripts allow-forms allow-popups allow-modals; "
    "script-src 'self' 'unsafe-inline' 'unsafe-eval'; child-src 'self';";

// The default CSP to be used if no CSP provided.
static const char kDefaultMV3CSP[] = "script-src 'self';";

// The minimum CSP to be used in order to prevent remote scripts.
static const char kMinimumMV3CSP[] =
    "script-src 'self' 'wasm-unsafe-eval' 'inline-speculation-rules'; "
    "object-src 'self';";
// The minimum CSP for extensions that can access chrome://resources.
static const char kMinimumMV3CSPWithChromeResources[] =
    "script-src 'self' chrome://resources 'wasm-unsafe-eval' "
    "'inline-speculation-rules'; "
    "object-src 'self';";
// The minimum CSP to be used in isolated worlds. The placeholder is for the
// extension's dynamic URL.
constexpr char kMinimumMV3IsolatedWorldCSPTemplate[] =
    "script-src 'self' 'wasm-unsafe-eval' 'inline-speculation-rules' %s; "
    "object-src 'self';";
// For unpacked extensions, we additionally allow the use of localhost files to
// aid in rapid local development.
static const char kMinimumUnpackedMV3CSP[] =
    "script-src 'self' 'wasm-unsafe-eval' 'inline-speculation-rules' "
    "http://localhost:* http://127.0.0.1:*; object-src 'self';";
// The minimum CSP to be used in isolated worlds for unpacked extensions. The
// placeholder is for the extension's dynamic URL.
constexpr char kMinimumUnpackedMV3IsolatedWorldCSPTemplate[] =
    "script-src 'self' 'wasm-unsafe-eval' 'inline-speculation-rules' "
    "http://localhost:* http://127.0.0.1:* %s; object-src 'self';";

#define PLATFORM_APP_LOCAL_CSP_SOURCES "'self' blob: filesystem: data:"

// clang-format off
const char kDefaultPlatformAppContentSecurityPolicy[] =
    // Platform apps can only use local resources by default.
    "default-src 'self' blob: filesystem:;"
    // For remote resources, they can fetch them via XMLHttpRequest.
    " connect-src * data: blob: filesystem:;"
    // And serve them via data: or same-origin (blob:, filesystem:) URLs
    " style-src " PLATFORM_APP_LOCAL_CSP_SOURCES " 'unsafe-inline';"
    " img-src " PLATFORM_APP_LOCAL_CSP_SOURCES ";"
    " frame-src " PLATFORM_APP_LOCAL_CSP_SOURCES ";"
    " font-src " PLATFORM_APP_LOCAL_CSP_SOURCES ";"
    // Media can be loaded from remote resources since:
    // 1. <video> and <audio> have good fallback behavior when offline or under
    //    spotty connectivity.
    // 2. Fetching via XHR and serving via blob: URLs currently does not allow
    //    streaming or partial buffering.
    " media-src * data: blob: filesystem:;"
    // Scripts are allowed to use WebAssembly
    " script-src 'self' blob: filesystem: 'wasm-unsafe-eval';";
// clang-format on

#undef PLATFORM_APP_LOCAL_CSP_SOURCES

int GetValidatorOptions(const Extension& extension) {
  CHECK_LE(extension.manifest_version(), 2);
  int options = csp_validator::OPTIONS_NONE;

  // crbug.com/40275793
  if (extension.GetType() == Manifest::Type::kExtension ||
      extension.GetType() == Manifest::Type::kLegacyPackagedApp) {
    options |= csp_validator::OPTIONS_ALLOW_UNSAFE_EVAL;
  }

  // Component extensions can specify an insecure object-src directive. This
  // should be safe because non-NPAPI plugins should load in a sandboxed process
  // and only allow communication via postMessage.
  if (extensions::Manifest::IsComponentLocation(extension.location())) {
    options |= csp_validator::OPTIONS_ALLOW_INSECURE_OBJECT_SRC;
  }

  return options;
}

std::u16string GetInvalidManifestKeyError(std::string_view key) {
  return ErrorUtils::FormatErrorMessageUTF16(errors::kInvalidManifestKey, key);
}

// Returns null if the manifest type can't access the path. Else returns the
// corresponding Value.
const base::Value* GetManifestPath(const Extension* extension,
                                   const char* path) {
  return extension->manifest()->FindPath(path);
}

const char* GetDefaultExtensionPagesCSP(const Extension& extension) {
  if (extension.manifest_version() >= 3) {
    return kDefaultMV3CSP;
  }

  if (extension.GetType() == Manifest::Type::kPlatformApp) {
    return kDefaultPlatformAppContentSecurityPolicy;
  }

  return kDefaultContentSecurityPolicy;
}

// Returns the minimum CSP to apply for the given MV3 extension.
const std::string* GetMinimumMV3CSPForExtension(const Extension& extension) {
  CHECK_GE(extension.manifest_version(), 3);

  if (csp_validator::IsExtensionAllowedToUseChromeResources(extension.id()) &&
      extension.location() == mojom::ManifestLocation::kComponent) {
    // The minimum CSP for these extensions should include access to
    // chrome://resources. This is okay because they are built into the browser
    // as component extensions.
    static const base::NoDestructor<std::string> csp_with_resources(
        kMinimumMV3CSPWithChromeResources);
    return csp_with_resources.get();
  }

  static const base::NoDestructor<std::string> default_csp(kMinimumMV3CSP);
  static const base::NoDestructor<std::string> default_unpacked_csp(
      kMinimumUnpackedMV3CSP);

  return Manifest::IsUnpackedLocation(extension.location())
             ? default_unpacked_csp.get()
             : default_csp.get();
}

}  // namespace

// static
const char* CSPInfo::kManifestDataKey = keys::kContentSecurityPolicy;

CSPInfo::CSPInfo(std::string extension_pages_csp, std::string sandbox_csp)
    : extension_pages_csp(std::move(extension_pages_csp)),
      sandbox_csp(std::move(sandbox_csp)) {}

CSPInfo::~CSPInfo() = default;

// static
const std::string& CSPInfo::GetExtensionPagesCSP(const Extension* extension) {
  const CSPInfo* csp_info = extension->GetManifestData<CSPInfo>();
  return csp_info ? csp_info->extension_pages_csp : base::EmptyString();
}

// static
const std::string* CSPInfo::GetMinimumCSPToAppend(
    const Extension& extension,
    const std::string& relative_path,
    bool is_service_worker) {
  if (!extension.is_extension()) {
    return nullptr;
  }

  // For sandboxed pages and manifest V2 extensions, append the parsed CSP. This
  // helps ensure that extension's can't get around our parsing rules by CSP
  // modifications through, say service workers.
  // We ignore the sandboxed page CSP for service workers, since they should
  // always be subject to the stricter extension CSP.
  if (!is_service_worker &&
      SandboxedPageInfo::IsSandboxedPage(&extension, relative_path)) {
    return &GetSandboxContentSecurityPolicy(&extension);
  }

  if (extension.manifest_version() <= 2) {
    return &GetExtensionPagesCSP(&extension);
  }

  // For manifest V3 extensions, append the minimum secure CSP. This
  // additionally helps protect against bugs in our CSP parsing code which may
  // cause the parsed CSP to not be as strong as the default one. For example,
  // see crbug.com/1042963.

  return GetMinimumMV3CSPForExtension(extension);
}

// static
std::optional<std::string> CSPInfo::GetIsolatedWorldCSP(
    const Extension& extension) {
  if (extension.manifest_version() >= 3) {
    std::string isolated_world_csp;
    // Note: base::StringPrintf() requires the template string to be constexpr,
    // so we can't put the template in a temporary const char* with a ternary
    // to avoid the repeated StringPrintf() calls.
    if (Manifest::IsUnpackedLocation(extension.location())) {
      isolated_world_csp =
          base::StringPrintf(kMinimumUnpackedMV3IsolatedWorldCSPTemplate,
                             extension.dynamic_url().spec().c_str());
    } else {
      isolated_world_csp =
          base::StringPrintf(kMinimumMV3IsolatedWorldCSPTemplate,
                             extension.dynamic_url().spec().c_str());
    }
    return isolated_world_csp;
  }

  Manifest::Type type = extension.GetType();
  bool bypass_main_world_csp = type == Manifest::Type::kPlatformApp ||
                               type == Manifest::Type::kExtension ||
                               type == Manifest::Type::kLegacyPackagedApp;
  if (!bypass_main_world_csp) {
    // The isolated world will use the main world CSP.
    return std::nullopt;
  }

  // The isolated world will bypass the main world CSP.
  return std::string();
}

// static
const std::string& CSPInfo::GetSandboxContentSecurityPolicy(
    const Extension* extension) {
  const CSPInfo* csp_info = extension->GetManifestData<CSPInfo>();
  return csp_info ? csp_info->sandbox_csp : base::EmptyString();
}

// static
const std::string& CSPInfo::GetResourceContentSecurityPolicy(
    const Extension* extension,
    const std::string& relative_path) {
  return SandboxedPageInfo::IsSandboxedPage(extension, relative_path)
             ? GetSandboxContentSecurityPolicy(extension)
             : GetExtensionPagesCSP(extension);
}

CSPHandler::CSPHandler() = default;

CSPHandler::~CSPHandler() = default;

// static
const char* CSPHandler::GetMinimumMV3CSPForTesting() {
  return kMinimumMV3CSP;
}

// static
const char* CSPHandler::GetMinimumUnpackedMV3CSPForTesting() {
  return kMinimumUnpackedMV3CSP;
}

bool CSPHandler::Parse(Extension* extension, std::u16string* error) {
  CHECK(error->empty());

  const char* key = extension->GetType() == Manifest::Type::kPlatformApp
                        ? keys::kPlatformAppContentSecurityPolicy
                        : keys::kContentSecurityPolicy;

  // The "content_security_policy" manifest key can either be a string or a
  // dictionary of the format.
  // "content_security_policy" : {
  //     "extension_pages": "",
  //     "sandbox": "",
  //  }
  // The dictionary is supported (and mandated) for manifest v3 (and above)
  // extensions.
  const base::Value* csp = GetManifestPath(extension, key);
  bool parse_as_dictionary = extension->manifest_version() >= 3;
  if (parse_as_dictionary) {
    if (csp && !csp->is_dict()) {
      *error = GetInvalidManifestKeyError(key);
      return false;
    }
    return ParseCSPDictionary(extension, error);
  }

  std::string extension_pages_csp;
  if (!ParseExtensionPagesCSP(extension, &extension_pages_csp, error, key,
                              csp)) {
    return false;
  }

  // Since this is a MV2 extension / app, we don't allow remote sources in
  // the CSP.
  constexpr bool kAllowRemoteSources = false;
  std::string sandbox_csp;
  if (!ParseSandboxCSP(extension, &sandbox_csp, error, keys::kSandboxedPagesCSP,
                       GetManifestPath(extension, keys::kSandboxedPagesCSP),
                       kAllowRemoteSources)) {
    return false;
  }

  extension->SetManifestData(
      std::make_unique<CSPInfo>(std::move(extension_pages_csp),
                                std::move(sandbox_csp)));
  return true;
}

bool CSPHandler::ParseCSPDictionary(Extension* extension,
                                    std::u16string* error) {
  // keys::kSandboxedPagesCSP shouldn't be used when using
  // keys::kContentSecurityPolicy as a dictionary.
  if (extension->manifest()->FindPath(keys::kSandboxedPagesCSP)) {
    *error = errors::kSandboxPagesCSPKeyNotAllowed;
    return false;
  }

  std::string extension_pages_csp;
  if (!ParseExtensionPagesCSP(
          extension, &extension_pages_csp, error,
          keys::kContentSecurityPolicy_ExtensionPagesPath,
          GetManifestPath(extension,
                          keys::kContentSecurityPolicy_ExtensionPagesPath))) {
    return false;
  }

  // Since this is an MV3 extension, we allow remote sources in the
  // sandboxed page CSP.
  constexpr bool kAllowRemoteSources = true;
  std::string sandbox_csp;
  if (!ParseSandboxCSP(
          extension, &sandbox_csp, error,
          keys::kContentSecurityPolicy_SandboxedPagesPath,
          GetManifestPath(extension,
                          keys::kContentSecurityPolicy_SandboxedPagesPath),
          kAllowRemoteSources)) {
    return false;
  }

  extension->SetManifestData(
      std::make_unique<CSPInfo>(std::move(extension_pages_csp),
                                std::move(sandbox_csp)));
  return true;
}

bool CSPHandler::ParseExtensionPagesCSP(
    Extension* extension,
    std::string* out_extension_pages_csp,
    std::u16string* error,
    std::string_view manifest_key,
    const base::Value* content_security_policy) {
  CHECK(error->empty());

  if (!content_security_policy) {
    const char* default_extension_pages_csp =
        GetDefaultExtensionPagesCSP(*extension);
    ValidateExtensionPagesCSP(*extension, manifest_key,
                              default_extension_pages_csp);
    *out_extension_pages_csp = default_extension_pages_csp;
    return true;
  }

  if (!content_security_policy->is_string()) {
    *error = GetInvalidManifestKeyError(manifest_key);
    return false;
  }

  const std::string& content_security_policy_str =
      content_security_policy->GetString();
  if (!ContentSecurityPolicyIsLegal(content_security_policy_str)) {
    *error = GetInvalidManifestKeyError(manifest_key);
    return false;
  }

  if (extension->manifest_version() >= 3) {
    if (!csp_validator::DoesCSPDisallowRemoteCode(
            extension->id(), extension->location(), content_security_policy_str,
            manifest_key, error)) {
      return false;
    }
    ValidateExtensionPagesCSP(*extension, manifest_key,
                              content_security_policy_str);
    *out_extension_pages_csp = content_security_policy_str;
    return true;
  }

  std::vector<InstallWarning> warnings;
  std::string sanitized_content_security_policy = SanitizeContentSecurityPolicy(
      content_security_policy_str, std::string(manifest_key),
      GetValidatorOptions(*extension), &warnings);
  extension->AddInstallWarnings(std::move(warnings));
  ValidateExtensionPagesCSP(*extension, manifest_key,
                            sanitized_content_security_policy);
  *out_extension_pages_csp = std::move(sanitized_content_security_policy);
  return true;
}

bool CSPHandler::ParseSandboxCSP(Extension* extension,
                                 std::string* out_sandbox_csp,
                                 std::u16string* error,
                                 std::string_view manifest_key,
                                 const base::Value* sandbox_csp,
                                 bool allow_remote_sources) {
  CHECK(error->empty());
  if (!sandbox_csp) {
    *out_sandbox_csp = kDefaultSandboxedPageContentSecurityPolicy;
    return true;
  }

  if (!sandbox_csp->is_string()) {
    *error = GetInvalidManifestKeyError(manifest_key);
    return false;
  }

  const std::string& sandbox_csp_str = sandbox_csp->GetString();
  if (!ContentSecurityPolicyIsLegal(sandbox_csp_str) ||
      !csp_validator::ContentSecurityPolicyIsSandboxed(sandbox_csp_str,
                                                       extension->GetType())) {
    *error = GetInvalidManifestKeyError(manifest_key);
    return false;
  }

  std::vector<InstallWarning> warnings;
  if (allow_remote_sources) {
    *out_sandbox_csp = sandbox_csp_str;
  } else {
    *out_sandbox_csp =
        csp_validator::GetSandboxedPageCSPDisallowingRemoteSources(
            sandbox_csp_str, std::string(manifest_key), &warnings);
  }
  extension->AddInstallWarnings(std::move(warnings));
  return true;
}

void CSPHandler::ValidateExtensionPagesCSP(
    const Extension& extension,
    std::string_view manifest_key,
    const std::string& content_security_policy) {
  if (extension.manifest_version() >= 3) {
    std::u16string error;
    CHECK(csp_validator::DoesCSPDisallowRemoteCode(
        extension.id(), extension.location(), content_security_policy,
        manifest_key, &error));
  } else {
    CHECK_EQ(content_security_policy,
             SanitizeContentSecurityPolicy(
                 content_security_policy, std::string(manifest_key),
                 GetValidatorOptions(extension), nullptr));
  }
}

bool CSPHandler::AlwaysParseForType(Manifest::Type type) const {
  return type == Manifest::Type::kPlatformApp ||
         type == Manifest::Type::kExtension ||
         type == Manifest::Type::kLegacyPackagedApp ||
         type == Manifest::Type::kUserScript;
}

base::span<const char* const> CSPHandler::Keys() const {
  static constexpr const char* kKeys[] = {
      keys::kContentSecurityPolicy, keys::kPlatformAppContentSecurityPolicy,
      keys::kSandboxedPagesCSP};
  return kKeys;
}

}  // namespace extensions
