Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInadequate encryption strength in Notifications
DescriptionInadequate encryption strength in Notifications
ComponentNotifications
Bug ClassLogic Error
Tracker502805441
Fix commit2123d26a8354 (chromium/src) +36/-10
CISA KEVNot listed
CreditedGoogle
Disclosed2026-08-25

Files Changed

  • chrome/browser/notifications/notification_platform_bridge_win.cc
  • chrome/browser/notifications/notification_platform_bridge_win_unittest.cc
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>(
Loading diff…

Regression Test / PoC

shipped with the fix
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>(
Loading diff…

Original Bug Report

reported by [email protected]

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.cc
  • content/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:

  1. Suppress Victim Notifications: Chrome’s GetToastNotification iterates 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 calls put_SuppressPopup(true), silently hiding the victim’s alert (e.g., a 2FA prompt).
  2. 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.
  3. 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.

  1. Identify the notification_id format used by https://victim.example (e.g., p#https://victim.example/#1alert).
  2. Determine the length of the victim’s notification_id.
  3. Offline, construct an attacker prefix: p#https://attacker.example/#1<crafted_tag> that matches the victim’s length.
  4. Run a brute-force search against the SuperFastHash algorithm. Find a <crafted_tag> such that the internal 32-bit state of SuperFastHash after processing the attacker’s notification_id exactly matches the internal state after processing the victim’s notification_id.
  5. Grant Notification permissions to https://attacker.example and display a notification using the <crafted_tag>.
  6. Trigger the legitimate notification from https://victim.example.
  7. 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.

View on issue tracker