CVE-2026-79084
Overview
Files Changed
chrome/browser/notifications/notification_platform_bridge_win.ccchrome/browser/notifications/notification_platform_bridge_win_unittest.cc
Patch
From 2123d26a835468a4564880ae6b4bbd4a5c51083d Mon Sep 17 00:00:00 2001 From: Joe Mason <[email protected]> Date: Fri, 24 Jul 2026 09:57:59 -0700 Subject: [PATCH] Use SHA256 hash to generate notification tags Fixed: 502805441 Change-Id: I42495b47b5d1246164f5c0c9feba232ecd2a92c8 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8141202 Auto-Submit: Joe Mason <[email protected]> Commit-Queue: Joe Mason <[email protected]> Reviewed-by: Peter Beverloo <[email protected]> Cr-Commit-Position: refs/heads/main@{#1667938} --- diff --git a/chrome/browser/notifications/notification_platform_bridge_win.cc b/chrome/browser/notifications/notification_platform_bridge_win.cc index 18564b39..e7e4b67a 100644 --- a/chrome/browser/notifications/notification_platform_bridge_win.cc +++ b/chrome/browser/notifications/notification_platform_bridge_win.cc @@ -15,6 +15,7 @@ #include <utility> #include <vector> +#include "base/base64.h" #include "base/base_paths_win.h" #include "base/command_line.h" #include "base/feature_list.h" @@ -22,12 +23,10 @@ #include "base/files/file_util.h" #include "base/functional/bind.h" #include "base/functional/callback_helpers.h" -#include "base/hash/hash.h" #include "base/i18n/file_util_icu.h" #include "base/logging.h" #include "base/metrics/histogram_functions.h" #include "base/path_service.h" -#include "base/strings/string_number_conversions.h" #include "base/strings/string_util.h" #include "base/strings/stringprintf.h" #include "base/strings/utf_string_conversions.h" @@ -63,6 +62,7 @@ #include "components/webapps/common/web_app_id.h" #include "content/public/browser/browser_task_traits.h" #include "content/public/browser/browser_thread.h" +#include "crypto/hash.h" #include "ui/message_center/public/cpp/notification.h" #include "url/origin.h" @@ -920,7 +920,9 @@ std::string payload = base::StringPrintf( "%s|%s|%s|%d", notification_id.c_str(), profile_id.c_str(), base::WideToUTF8(app_user_model_id).c_str(), incognito); - return base::NumberToWString(base::Hash(payload)); + // The tag has a max length of 63 characters (plus null terminator). + // Base64Encode yields 44 chars. + return base::ASCIIToWide(base::Base64Encode(crypto::hash::Sha256(payload))); } HRESULT OnFailed(winui::Notifications::IToastNotification* notification, diff --git a/chrome/browser/notifications/notification_platform_bridge_win_unittest.cc b/chrome/browser/notifications/notification_platform_bridge_win_unittest.cc index 9ed3a37..7704d56 100644 --- a/chrome/browser/notifications/notification_platform_bridge_win_unittest.cc +++ b/chrome/browser/notifications/notification_platform_bridge_win_unittest.cc @@ -10,12 +10,12 @@ #include <memory> #include <string> +#include <string_view> #include <utility> #include <vector> -#include "base/hash/hash.h" +#include "base/base64.h" #include "base/logging.h" -#include "base/strings/string_number_conversions.h" #include "base/strings/utf_string_conversions.h" #include "base/win/scoped_com_initializer.h" #include "base/win/scoped_hstring.h" @@ -25,6 +25,7 @@ #include "chrome/browser/notifications/win/notification_launch_id.h" #include "chrome/browser/notifications/win/notification_template_builder.h" #include "content/public/test/browser_task_environment.h" +#include "crypto/hash.h" #include "testing/gtest/include/gtest/gtest.h" #include "ui/message_center/public/cpp/notification.h" #include "ui/message_center/public/cpp/notifier_id.h" @@ -65,13 +66,14 @@ bool renotify, const std::string& profile_id, const std::wstring& app_user_model_id, - bool incognito) { + bool incognito, + std::string_view notification_id = kNotificationId) { DCHECK(bridge); GURL origin(kOrigin); auto notification = std::make_unique<message_center::Notification>( - message_center::NOTIFICATION_TYPE_SIMPLE, kNotificationId, u"title", - u"message", ui::ImageModel(), u"display_source", origin, + message_center::NOTIFICATION_TYPE_SIMPLE, notification_id.data(), + u"title", u"message", ui::ImageModel(), u"display_source", origin, message_center::NotifierId(origin), message_center::RichNotificationData(), nullptr /* delegate */); notification->set_renotify(renotify); @@ -127,7 +129,9 @@ base::win::ScopedHString tag(hstring_tag); std::string tag_data = std::string(kNotificationId) + "|" + kProfileId + "|" + kAppUserModelIdUTF8 + "|0"; - ASSERT_EQ(base::NumberToWString(base::Hash(tag_data)), tag.Get()); + ASSERT_EQ( + base::ASCIIToWide(base::Base64Encode(crypto::hash::Sha256(tag_data))), + tag.Get()); // Let tasks on |notification_task_runner_| of |bridge| run before its dtor. task_environment_.RunUntilIdle(); @@ -223,6 +227,25 @@ ASSERT_NE(tagA.Get(), tagB.Get()); } + // Same profile, different id -> Unique tags. + { + toastA = GetToast(&bridge, launch_id, /*renotify=*/false, "Profile1", + kAppUserModelId, /*incognito=*/true, "notification1"); + toastB = GetToast(&bridge, launch_id, /*renotify=*/false, "Profile1", + kAppUserModelId, /*incognito=*/true, "notification2"); + + ASSERT_TRUE(toastA); + ASSERT_TRUE(toastB); + + ASSERT_HRESULT_SUCCEEDED(toastA->get_Tag(&hstring_tagA)); + base::win::ScopedHString tagA(hstring_tagA); + + ASSERT_HRESULT_SUCCEEDED(toastB->get_Tag(&hstring_tagB)); + base::win::ScopedHString tagB(hstring_tagB); + + ASSERT_NE(tagA.Get(), tagB.Get()); + } + // Let tasks on |notification_task_runner_| of |bridge| run before its dtor. task_environment_.RunUntilIdle(); } @@ -254,7 +277,8 @@ // Register a single notification with a specific tag. std::string tag_data = std::string(kNotificationId) + "|" + kProfileId + "|" + kAppUserModelIdUTF8 + "|0"; - std::wstring tag = base::NumberToWString(base::Hash(tag_data)); + std::wstring tag = + base::ASCIIToWide(base::Base64Encode(crypto::hash::Sha256(tag_data))); // Microsoft::WRL::Make() requires FakeIToastNotification to derive from // RuntimeClass. notifications.push_back(Microsoft::WRL::Make<FakeIToastNotification>(
Regression Test / PoC
diff --git a/chrome/browser/notifications/notification_platform_bridge_win_unittest.cc b/chrome/browser/notifications/notification_platform_bridge_win_unittest.cc
index 9ed3a37..7704d56 100644
--- a/chrome/browser/notifications/notification_platform_bridge_win_unittest.cc
+++ b/chrome/browser/notifications/notification_platform_bridge_win_unittest.cc
@@ -10,12 +10,12 @@
#include <memory>
#include <string>
+#include <string_view>
#include <utility>
#include <vector>
-#include "base/hash/hash.h"
+#include "base/base64.h"
#include "base/logging.h"
-#include "base/strings/string_number_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/win/scoped_com_initializer.h"
#include "base/win/scoped_hstring.h"
@@ -25,6 +25,7 @@
#include "chrome/browser/notifications/win/notification_launch_id.h"
#include "chrome/browser/notifications/win/notification_template_builder.h"
#include "content/public/test/browser_task_environment.h"
+#include "crypto/hash.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/message_center/public/cpp/notification.h"
#include "ui/message_center/public/cpp/notifier_id.h"
@@ -65,13 +66,14 @@
bool renotify,
const std::string& profile_id,
const std::wstring& app_user_model_id,
- bool incognito) {
+ bool incognito,
+ std::string_view notification_id = kNotificationId) {
DCHECK(bridge);
GURL origin(kOrigin);
auto notification = std::make_unique<message_center::Notification>(
- message_center::NOTIFICATION_TYPE_SIMPLE, kNotificationId, u"title",
- u"message", ui::ImageModel(), u"display_source", origin,
+ message_center::NOTIFICATION_TYPE_SIMPLE, notification_id.data(),
+ u"title", u"message", ui::ImageModel(), u"display_source", origin,
message_center::NotifierId(origin),
message_center::RichNotificationData(), nullptr /* delegate */);
notification->set_renotify(renotify);
@@ -127,7 +129,9 @@
base::win::ScopedHString tag(hstring_tag);
std::string tag_data = std::string(kNotificationId) + "|" + kProfileId + "|" +
kAppUserModelIdUTF8 + "|0";
- ASSERT_EQ(base::NumberToWString(base::Hash(tag_data)), tag.Get());
+ ASSERT_EQ(
+ base::ASCIIToWide(base::Base64Encode(crypto::hash::Sha256(tag_data))),
+ tag.Get());
// Let tasks on |notification_task_runner_| of |bridge| run before its dtor.
task_environment_.RunUntilIdle();
@@ -223,6 +227,25 @@
ASSERT_NE(tagA.Get(), tagB.Get());
}
+ // Same profile, different id -> Unique tags.
+ {
+ toastA = GetToast(&bridge, launch_id, /*renotify=*/false, "Profile1",
+ kAppUserModelId, /*incognito=*/true, "notification1");
+ toastB = GetToast(&bridge, launch_id, /*renotify=*/false, "Profile1",
+ kAppUserModelId, /*incognito=*/true, "notification2");
+
+ ASSERT_TRUE(toastA);
+ ASSERT_TRUE(toastB);
+
+ ASSERT_HRESULT_SUCCEEDED(toastA->get_Tag(&hstring_tagA));
+ base::win::ScopedHString tagA(hstring_tagA);
+
+ ASSERT_HRESULT_SUCCEEDED(toastB->get_Tag(&hstring_tagB));
+ base::win::ScopedHString tagB(hstring_tagB);
+
+ ASSERT_NE(tagA.Get(), tagB.Get());
+ }
+
// Let tasks on |notification_task_runner_| of |bridge| run before its dtor.
task_environment_.RunUntilIdle();
}
@@ -254,7 +277,8 @@
// Register a single notification with a specific tag.
std::string tag_data = std::string(kNotificationId) + "|" + kProfileId + "|" +
kAppUserModelIdUTF8 + "|0";
- std::wstring tag = base::NumberToWString(base::Hash(tag_data));
+ std::wstring tag =
+ base::ASCIIToWide(base::Base64Encode(crypto::hash::Sha256(tag_data)));
// Microsoft::WRL::Make() requires FakeIToastNotification to derive from
// RuntimeClass.
notifications.push_back(Microsoft::WRL::Make<FakeIToastNotification>(
Original Bug Report
Potential cross-origin notification interference via hash collision on Windows
Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports without the Chrome Security team. Please see go/chrome-ai-generated-security-bugs-faq for more information.
Overview: Chrome generates a Windows notification Tag by using an unkeyed, 32-bit hash (base::Hash) of a string containing the notification ID and origin. An attacker with Notification permissions can exploit the structural weaknesses of this hash to predictably generate collisions with a victim’s notifications, allowing cross-origin suppression or replacement.
Affected files:
chrome/browser/notifications/notification_platform_bridge_win.cccontent/browser/notifications/notification_id_generator.cc
Estimated timestamp from git blame: 2023-11-09
Description
On Windows, Chrome assigns every notification a Tag to uniquely identify it within the native Windows Action Center. In NotificationPlatformBridgeWinImpl::GetTag, this Tag is generated by calculating a 32-bit base::Hash of a concatenated string:
// chrome/browser/notifications/notification_platform_bridge_win.cc
std::wstring GetTag(const std::string& notification_id,
const std::string& profile_id,
const std::wstring& app_user_model_id,
bool incognito) {
std::string payload = base::StringPrintf(
"%s|%s|%s|%d", notification_id.c_str(), profile_id.c_str(),
base::WideToUTF8(app_user_model_id).c_str(), incognito);
return base::NumberToWString(base::Hash(payload));
}
base::Hash relies on the SuperFastHash algorithm. This algorithm is deterministic, unkeyed, and has only a 32-bit internal state.
The notification_id embeds the web origin and a developer-provided tag (e.g., p#https://example.com/#1<tag>). The remaining fields (profile_id, app_user_model_id, and incognito) are generally identical for all standard web origins within the same browser profile.
Because the internal state of SuperFastHash is only 32 bits, an attacker can intentionally craft a <tag> for their own origin (https://attacker.com) such that the hash of their payload perfectly matches the hash of a victim origin’s payload (https://victim.com).
Crucially, because SuperFastHash processes data sequentially, the attacker does not even need to know the unpredictable parts of the suffix (like the user’s specific AUMI). By making the attacker’s notification_id the exact same length as the victim’s, the attacker can force an internal state collision at the | boundary. Once the internal state collides, any identical suffix appended to both strings will result in the same final hash.
Impact
A cross-origin hash collision breaks the isolation between origins in the notification system. If an attacker (https://attacker.example) creates a notification that collides with a victim (https://victim.example), the attacker can:
- Suppress Victim Notifications: Chrome’s
GetToastNotificationiterates through active notifications to prevent duplicate sounds/popups. If it sees the attacker’s colliding Tag, it assumes the victim’s new notification is just a minor update and callsput_SuppressPopup(true), silently hiding the victim’s alert (e.g., a 2FA prompt). - Replace Victim Notifications: Windows will treat the colliding Tag as the same entity, allowing the attacker to overwrite an active victim notification in the Action Center with attacker-controlled text.
- Dismiss Victim Notifications: If the attacker calls
notification.close(), Chrome instructs Windows to remove the colliding Tag, prematurely closing the victim’s active notification.
Suggested Reproduction Steps
Note: These are potential steps as our tooling does not currently run exploit code.
- Identify the
notification_idformat used byhttps://victim.example(e.g.,p#https://victim.example/#1alert). - Determine the length of the victim’s
notification_id. - Offline, construct an attacker prefix:
p#https://attacker.example/#1<crafted_tag>that matches the victim’s length. - Run a brute-force search against the
SuperFastHashalgorithm. Find a<crafted_tag>such that the internal 32-bit state ofSuperFastHashafter processing the attacker’snotification_idexactly matches the internal state after processing the victim’snotification_id. - Grant Notification permissions to
https://attacker.exampleand display a notification using the<crafted_tag>. - Trigger the legitimate notification from
https://victim.example. - Observe that the victim’s notification is suppressed or overwritten by the attacker’s notification in the Windows Action Center.
Suggested Fix
Do not use a 32-bit, collision-prone hash for identifying security-sensitive cross-origin resources. The Tag should be generated using a cryptographically secure hash (e.g., SHA-256) of the payload, truncated or encoded to fit Windows API limits if necessary, or the system should track a secure mapping between the notification_id and a randomly generated GUID used as the Windows Tag.
Evaluated with Chrome root at commit: 661452647ddb2827305122ff3273bd5dea403f09
Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.