blob: a8e248d4a1c87d4b79bf92467c99181e09181961 [file] [log] [blame]
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/safe_browsing/notification_telemetry/notification_telemetry_service.h"
#include "base/check.h"
#include "base/logging.h"
#include "base/metrics/histogram_functions.h"
#include "base/strings/escape.h"
#include "base/time/time.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/push_messaging/push_messaging_service_factory.h"
#include "chrome/browser/push_messaging/push_messaging_service_impl.h"
#include "chrome/browser/safe_browsing/notification_telemetry/notification_telemetry_service_factory.h"
#include "components/safe_browsing/content/browser/notification_content_detection/notifications_global_cache_list.h"
#include "components/safe_browsing/core/browser/db/database_manager.h"
#include "components/safe_browsing/core/common/proto/csd.pb.h"
#include "components/safe_browsing/core/common/safe_browsing_prefs.h"
#include "components/safe_browsing/core/common/utils.h"
#include "content/public/browser/service_worker_context.h"
#include "content/public/browser/service_worker_registration_information.h"
#include "content/public/browser/storage_partition.h"
#include "google_apis/google_api_keys.h"
#include "net/base/load_flags.h"
#include "net/http/http_status_code.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "services/network/public/cpp/simple_url_loader.h"
#include "services/network/public/mojom/url_response_head.mojom.h"
#include "url/gurl.h"
#include "url/origin.h"
namespace safe_browsing {
ServiceWorkerTelemetryInfo::ServiceWorkerTelemetryInfo() noexcept = default;
ServiceWorkerTelemetryInfo::ServiceWorkerTelemetryInfo(
const ServiceWorkerTelemetryInfo& other) noexcept = default;
ServiceWorkerTelemetryInfo::~ServiceWorkerTelemetryInfo() = default;
namespace {
// The maximum number of times MaybeUploadReport can encounter an empty database
// before stopping the timer.
const int kMaxEmptyDbFoundCount = 2;
// Size of the stored service worker info cache.
const int kNotificationTelemetryServiceWorkerInfoMaxCount = 20;
const char kSbIncidentReportUrl[] =
"https://sb-ssl.google.com/safebrowsing/clientreport/incident";
constexpr net::NetworkTrafficAnnotationTag
kSafeBrowsingIncidentTrafficAnnotation =
net::DefineNetworkTrafficAnnotation("notification_telemetry", R"(
semantics {
sender: "Notification Telemetry Service"
description:
"Chrome will upload registration data for service workers that "
"subscribe to push messages. The data uploaded consists of the "
"service worker scope URL and the URLs of the scripts imported "
"by the service worker during installation. This data is only "
"collected if the service worker's scope URL is not on the "
"Safe Browsing allowlist. It will be used to detect websites "
"that install service workers to display abusive notifications."
trigger:
"User navigates to a website that installs a service worker "
"to display push notifications."
data:
"The service worker scope URL and the URLs of the imported "
"scripts that don't match the scope origin. See "
"ServiceWorkerRegistrationIncident in 'https://cs.chromium.org/ "
"chromium/src/components/safe_browsing/csd.proto' for details."
destination: GOOGLE_OWNED_SERVICE
internal {
contacts {
owners: "chrome-counter-abuse-alerts@google.com"
}
}
user_data {
type: SENSITIVE_URL
}
last_reviewed: "2025-04-28"
}
policy {
cookies_allowed: YES
cookies_store: "Safe Browsing cookie store"
setting:
"Users can enable this feature by selecting the "
"'Enhanced protection' option in "
"'Settings->Privacy and security->Security->Safe Browsing`. "
"The feature is disabled by default because the default "
"Safe Browsing setting is 'Standard protection'."
chrome_policy {
SafeBrowsingEnabled {
policy_options {mode: MANDATORY}
SafeBrowsingEnabled: false
}
}
})");
} // namespace
// static
NotificationTelemetryService* NotificationTelemetryService::Get(
Profile* profile) {
return NotificationTelemetryServiceFactory::GetInstance()->GetForProfile(
profile);
}
NotificationTelemetryService::NotificationTelemetryService(
Profile* profile,
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
scoped_refptr<SafeBrowsingDatabaseManager> database_manager,
std::unique_ptr<NotificationTelemetryStoreInterface> telemetry_store,
scoped_refptr<SafeBrowsingUIManager> ui_manager)
: url_loader_factory_(url_loader_factory),
database_manager_(database_manager),
telemetry_store_(std::move(telemetry_store)),
ui_manager_(ui_manager),
profile_(profile) {
service_worker_context_ =
profile_->GetDefaultStoragePartition()->GetServiceWorkerContext();
service_worker_context_->AddObserver(this);
PushMessagingServiceImpl* push_messaging_service =
PushMessagingServiceFactory::GetForProfile(profile_);
// Notification Telemetry Service is a keyed service and will outlive
// any invocations of the callback being registered with the push messaging
// service (also a keyed service).
push_messaging_service->SetSubscribeFromWorkerCallback(base::BindRepeating(
&NotificationTelemetryService::OnNewNotificationServiceWorkerSubscription,
base::Unretained(this)));
if (telemetry_store_ && ui_manager_) {
MaybeUploadReport();
}
}
NotificationTelemetryService::~NotificationTelemetryService() {
service_worker_context_->RemoveObserver(this);
}
void NotificationTelemetryService::OnRegistrationStored(
int64_t registration_id,
const GURL& scope,
const content::ServiceWorkerRegistrationInformation&
service_worker_registration_info) {
// Only collect information for ESB users
if (!IsEnhancedProtectionEnabled(*profile_->GetPrefs())) {
return;
}
// Check feature flag after ESB check so that the Finch experiment
// groups only include clients that can send telemetry reports.
if (!base::FeatureList::IsEnabled(safe_browsing::kNotificationTelemetry)) {
return;
}
// Check that at least one of the resources belongs to an external domain
bool external_resource = false;
url::Origin scope_origin = url::Origin::Create(scope);
for (auto& resource : service_worker_registration_info.resources) {
url::Origin resource_url = url::Origin::Create(resource);
if (resource_url != scope_origin) {
external_resource = true;
break;
}
}
// Check with safe browsing to see if the origin is allowlisted.
if (external_resource) {
ServiceWorkerTelemetryInfo service_worker_info;
service_worker_info.scope = scope;
service_worker_info.registration_id = registration_id;
service_worker_info.resources = service_worker_registration_info.resources;
// TODO(crbug.com/433543634): Clean up the use of `database_manager_` post
// GlobalCacheListForGatingNotificationProtections launch.
if (database_manager_ == nullptr) {
MaybeStoreServiceWorkerInfo(
service_worker_info,
ShouldSkipNotificationProtectionsDueToGlobalCacheList(scope));
} else {
database_manager_->CheckUrlForHighConfidenceAllowlist(
scope,
base::BindOnce(&NotificationTelemetryService::DatabaseCheckDone,
weak_factory_.GetWeakPtr(), service_worker_info));
}
}
}
void NotificationTelemetryService::OnAddServiceWorkerBehavior(bool success) {
if (success) {
// Since there is now at least one new ServiceWorkerBehavior in storage,
// start the timer to send a report.
if (!timer_.IsRunning()) {
timer_.Start(
FROM_HERE,
base::Minutes(kNotificationTelemetrySwbPollingInterval.Get()), this,
&NotificationTelemetryService::MaybeUploadReport);
}
empty_db_found_count_ = 0;
}
}
void NotificationTelemetryService::OnPushEventFinished(
const GURL& script_url,
const std::optional<std::vector<GURL>>& requested_urls) {
if (!requested_urls.has_value()) {
return;
}
if (!base::FeatureList::IsEnabled(safe_browsing::kNotificationTelemetrySwb)) {
return;
}
// Store the network request.
if (telemetry_store_) {
telemetry_store_->AddServiceWorkerPushBehavior(
script_url, requested_urls.value(),
base::BindOnce(
&NotificationTelemetryService::OnAddServiceWorkerBehavior,
weak_factory_.GetWeakPtr()));
}
}
// static
int NotificationTelemetryService::ServiceWorkerInfoCacheSizeForTest() {
return kNotificationTelemetryServiceWorkerInfoMaxCount;
}
NotificationTelemetryStoreInterface*
NotificationTelemetryService::GetTelemetryStoreForTest() {
return telemetry_store_.get();
}
int NotificationTelemetryService::GetEmptyDbFoundCountForTest() {
return empty_db_found_count_;
}
void NotificationTelemetryService::DatabaseCheckDone(
ServiceWorkerTelemetryInfo service_worker_info,
bool allow_listed,
std::optional<
SafeBrowsingDatabaseManager::HighConfidenceAllowlistCheckLoggingDetails>
logging_details) {
MaybeStoreServiceWorkerInfo(service_worker_info, allow_listed);
}
void NotificationTelemetryService::MaybeStoreServiceWorkerInfo(
ServiceWorkerTelemetryInfo service_worker_info,
bool allow_listed) {
base::UmaHistogramBoolean(
"SafeBrowsing.NotificationTelemetry.ServiceWorkerScopeURL.IsAllowlisted",
allow_listed);
// No handling required for service workers with allowlisted scope URLs.
if (allow_listed) {
return;
}
// Only store up to `kNotificationTelemetryServiceWorkerInfoMaxCount` entries.
// Remove the oldest entry in the store if necessary to accommodate a new one.
if (service_worker_infos_.size() >=
kNotificationTelemetryServiceWorkerInfoMaxCount) {
service_worker_infos_.erase(service_worker_infos_.begin());
}
service_worker_infos_.push_back(service_worker_info);
}
void NotificationTelemetryService::OnNewNotificationServiceWorkerSubscription(
int64_t registration_id) {
// Only collect information for ESB users
if (!IsEnhancedProtectionEnabled(*profile_->GetPrefs())) {
return;
}
// Check feature flag after ESB check so that the Finch experiment
// groups only include clients that can send telemetry reports.
if (!base::FeatureList::IsEnabled(safe_browsing::kNotificationTelemetry)) {
return;
}
// Check the stored service worker list to see if there is an
// entry that has the same registration id for which we received
// the notification.
auto it =
std::find_if(service_worker_infos_.begin(), service_worker_infos_.end(),
[registration_id](const ServiceWorkerTelemetryInfo& info) {
return registration_id == info.registration_id;
});
// No match found, so return without doing anything.
if (it == service_worker_infos_.end()) {
return;
}
// Match found, save the matched registration data
// and delete the matched entry in the stored list.
ServiceWorkerTelemetryInfo report_data = std::move(*it);
service_worker_infos_.erase(it);
// Store `import_script_url` to be sent as ServiceWorkerBehavior in
// ClientSafeBrowsingReportRequest.
if (base::FeatureList::IsEnabled(safe_browsing::kNotificationTelemetrySwb) &&
telemetry_store_) {
telemetry_store_->AddServiceWorkerRegistrationBehavior(
report_data.scope, report_data.resources,
base::BindOnce(
&NotificationTelemetryService::OnAddServiceWorkerBehavior,
weak_factory_.GetWeakPtr()));
}
// Generate a telemetry report with the matched data.
// ClientIncidentReports will continue to be sent for the same events with
// ServiceWorkerBehavior reports during kNotificationTelemetrySwb launch.
// TODO(crbug.com/424167989): Turn down sending of ClientIncidentReports once
// `kNotificationTelemetrySwb` is fully launched and verified.
std::unique_ptr<ClientIncidentReport_IncidentData> incident_data =
std::make_unique<ClientIncidentReport_IncidentData>();
ClientIncidentReport_IncidentData_ServiceWorkerRegistrationIncident*
notification_resource_report =
incident_data->mutable_notification_import_script();
notification_resource_report->set_scope_url(report_data.scope.spec());
for (const auto& resource : report_data.resources) {
std::string* import_script_url =
notification_resource_report->add_import_script_url();
*import_script_url = resource.spec();
}
std::unique_ptr<ClientIncidentReport> report =
std::make_unique<ClientIncidentReport>();
report->mutable_incident()->AddAllocated(incident_data.release());
std::string post_data;
if (!report->SerializeToString(&post_data)) {
return;
}
// Send report for upload
auto resource_request = std::make_unique<network::ResourceRequest>();
resource_request->url = GetTelemetryReportUrl();
resource_request->method = "POST";
resource_request->load_flags = net::LOAD_DISABLE_CACHE;
resource_request->credentials_mode = network::mojom::CredentialsMode::kOmit;
url_loader_ = network::SimpleURLLoader::Create(
std::move(resource_request), kSafeBrowsingIncidentTrafficAnnotation);
url_loader_->AttachStringForUpload(post_data, "application/octet-stream");
// Using base::Unretained is safe here as Network Telemetry Service owns
// `url_loader_`.
url_loader_->DownloadToStringOfUnboundedSizeUntilCrashAndDie(
url_loader_factory_.get(),
base::BindOnce(&NotificationTelemetryService::UploadComplete,
base::Unretained(this)));
}
void NotificationTelemetryService::UploadComplete(
std::unique_ptr<std::string> response_body) {
// Take ownership of the loader in this scope.
std::unique_ptr<network::SimpleURLLoader> url_loader(std::move(url_loader_));
int response_code = 0;
if (url_loader->ResponseInfo() && url_loader->ResponseInfo()->headers) {
response_code = url_loader->ResponseInfo()->headers->response_code();
}
RecordHttpResponseOrErrorCode(
"SafeBrowsing.NotificationTelemetry.NetworkResult",
url_loader->NetError(), response_code);
}
void NotificationTelemetryService::OnTelemetryStoreDeleted(bool success) {
// If the attempt to delete the store failed, try again once.
if (!success) {
telemetry_store_->DeleteAll(base::DoNothing());
}
empty_db_found_count_ = 0;
}
void NotificationTelemetryService::OnGetServiceWorkerBehaviors(
bool success,
std::unique_ptr<std::vector<CSBRR::ServiceWorkerBehavior>> entries) {
if (!success) {
return;
}
if (!entries || entries->size() == 0) {
empty_db_found_count_++;
return;
}
auto report = std::make_unique<CSBRR>();
report->set_type(CSBRR::SERVICE_WORKER_BEHAVIOR);
base::flat_map<std::string, CSBRR::ServiceWorkerBehavior*> messages;
for (const auto& entry : *entries) {
if (messages.contains(entry.script_url())) {
messages[entry.script_url()]->MergeFrom(entry);
continue;
}
CSBRR::ServiceWorkerBehavior* service_worker_behavior =
report->add_service_worker_behaviors();
service_worker_behavior->CopyFrom(entry);
// Aggregate ServiceWorkerBehavior reports by `script_url`. Note that
// ServiceWorkerBehavior reports from registration events do not contain
// `script_url`.
if (entry.has_script_url()) {
messages.emplace(entry.script_url(), service_worker_behavior);
}
}
std::string serialized_report;
if (report->SerializeToString(&serialized_report)) {
// Log report size to enable server-side capacity planning.
base::UmaHistogramCounts1M("SafeBrowsing.NotificationTelemetry.CSBRR.Size",
serialized_report.size());
}
if (ui_manager_ && profile_ && kNotificationTelemetrySwbSendReports.Get()) {
ui_manager_->SendThreatDetails(profile_, std::move(report));
}
// Whether we've sent a report or not, clear the database to avoid build up.
telemetry_store_->DeleteAll(base::DoNothing());
}
void NotificationTelemetryService::MaybeUploadReport() {
// Account for the case where the user stops being an ESB user.
if (!safe_browsing::IsEnhancedProtectionEnabled(*profile_->GetPrefs())) {
timer_.Stop();
if (telemetry_store_) {
telemetry_store_->DeleteAll(
base::BindOnce(&NotificationTelemetryService::OnTelemetryStoreDeleted,
weak_factory_.GetWeakPtr()));
}
return;
}
// If polling in repeated succession turns up nothing, stop.
if (empty_db_found_count_ > kMaxEmptyDbFoundCount) {
timer_.Stop();
return;
}
// Now, check the database.
if (telemetry_store_) {
telemetry_store_->GetServiceWorkerBehaviors(
base::BindOnce(
&NotificationTelemetryService::OnGetServiceWorkerBehaviors,
weak_factory_.GetWeakPtr()),
base::DoNothing());
}
}
// static
GURL NotificationTelemetryService::GetTelemetryReportUrl() {
GURL url(kSbIncidentReportUrl);
std::string api_key(google_apis::GetAPIKey());
if (api_key.empty()) {
return url;
}
return url.Resolve("?key=" + base::EscapeQueryParamValue(api_key, true));
}
} // namespace safe_browsing