Overview

Low
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in Extensions
DescriptionInappropriate implementation in Extensions
ComponentExtensions
Bug ClassLogic Error
Tracker514073460
Fix commit81cc594d570c (chromium/src) +23/-19
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-30

Files Changed

  • chrome/browser/extensions/extension_loading_browsertest.cc
  • chrome/browser/extensions/extension_url_overrides_unittest.cc
  • chrome/browser/extensions/service_worker_registration_apitest.cc
  • chrome/browser/ui/extensions/settings_overridden_params_providers_unittest.cc
  • chrome/common/extensions/permissions/chrome_permission_message_provider.cc
From 81cc594d570cb7c59cc0eb50584f12577e0f0834 Mon Sep 17 00:00:00 2001
From: Tim Judkins <[email protected]>
Date: Tue, 19 May 2026 16:56:30 -0700
Subject: [PATCH] [Extensions] Remove old NTP permission upgrade code

This code was intended to be removed a long time ago and only existed to
allow for extensions using the old New Tab Page permission to update to
using the newer way of doing that with chrome_url_overrides.

This CL removes the old code and adjusts the associated test to ensure
an extension doing this now would be disabled for a permission increase.
It also updates a few tests which were previously skipping the
permission dialog on initial install of a NTP override extension and
then updating that extension, to instead grant the permission so that
the extension isn't disabled for a "permission increase" on update.

Fixed: 514073460
Change-Id: I6a53074c13f68271074b1543d20daf2102a4aca1
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7856958
Commit-Queue: Tim <[email protected]>
Reviewed-by: Devlin Cronin <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1633202}
---

diff --git a/chrome/browser/extensions/extension_loading_browsertest.cc b/chrome/browser/extensions/extension_loading_browsertest.cc
index d509ba6..4c914697 100644
--- a/chrome/browser/extensions/extension_loading_browsertest.cc
+++ b/chrome/browser/extensions/extension_loading_browsertest.cc
@@ -23,8 +23,10 @@
 #include "content/public/browser/devtools_agent_host.h"
 #include "content/public/test/browser_test.h"
 #include "content/public/test/browser_test_utils.h"
+#include "extensions/browser/disable_reason.h"
 #include "extensions/browser/event_router.h"
 #include "extensions/browser/extension_host.h"
+#include "extensions/browser/extension_prefs.h"
 #include "extensions/browser/extension_registry.h"
 #include "extensions/browser/process_manager.h"
 #include "extensions/common/manifest_handlers/background_info.h"
@@ -73,8 +75,8 @@
   extension_dir.WriteFile(FILE_PATH_LITERAL("newtab.html"),
                           "<h1>Overridden New Tab Page</h1>");
 
-  const Extension* new_tab_extension =
-      InstallExtension(extension_dir.Pack(), 1 /*new install*/);
+  const Extension* new_tab_extension = InstallExtensionWithPermissionsGranted(
+      extension_dir.Pack(), 1 /*new install*/);
   ASSERT_TRUE(new_tab_extension);
 
   // Visit the New Tab Page to get a renderer using the extension into history.
@@ -109,7 +111,7 @@
 }
 
 IN_PROC_BROWSER_TEST_F(ExtensionLoadingTest,
-                       UpgradeAddingNewTabPagePermissionNoPrompt) {
+                       UpgradeAddingNewTabPagePermissionDisablesExtension) {
   ASSERT_TRUE(embedded_test_server()->Start());
 
   TestExtensionDir extension_dir;
@@ -149,19 +151,22 @@
   extension_dir.WriteManifest(
       base::StringPrintf(kManifestTemplate, 2, kNtpOverrideString));
 
-  // Upgrade the extension, ensure that the upgrade 'worked' in the sense that
-  // the extension is still present and not disabled and that it now has the
-  // new API permission.
-  // TODO(robertshield): Update this once most of the population is on M62+
-  // and adding NTP permissions implies a permission upgrade.
-  new_tab_extension = UpdateExtension(
-      new_tab_extension->id(), extension_dir.Pack(), 0 /*expected upgrade*/);
+  std::string extension_id = new_tab_extension->id();
+
+  // Upgrade the extension, and ensure that adding NTP permissions implies a
+  // permission upgrade, so the extension is disabled.
+  UpdateExtension(extension_id, extension_dir.Pack(), -1 /*expected upgrade*/);
+
+  ExtensionRegistry* registry = ExtensionRegistry::Get(profile());
+  new_tab_extension = registry->disabled_extensions().GetByID(extension_id);
   ASSERT_NE(nullptr, new_tab_extension);
 
   EXPECT_TRUE(new_tab_extension->permissions_data()->HasAPIPermission(
       mojom::APIPermissionID::kNewTabPageOverride));
   EXPECT_THAT(new_tab_extension->version().components(),
               testing::ElementsAre(2));
+  EXPECT_TRUE(ExtensionPrefs::Get(profile())->HasDisableReason(
+      extension_id, disable_reason::DISABLE_PERMISSIONS_INCREASE));
 }
 
 // Tests the behavior described in http://crbug.com/41201916.
diff --git a/chrome/browser/extensions/extension_url_overrides_unittest.cc b/chrome/browser/extensions/extension_url_overrides_unittest.cc
index fac625b4..2f9c15d 100644
--- a/chrome/browser/extensions/extension_url_overrides_unittest.cc
+++ b/chrome/browser/extensions/extension_url_overrides_unittest.cc
@@ -26,6 +26,7 @@
 #include "extensions/browser/extension_registrar.h"
 #include "extensions/browser/extension_system.h"
 #include "extensions/browser/mock_external_provider.h"
+#include "extensions/browser/permissions/permissions_updater.h"
 #include "extensions/browser/test_extension_registry_observer.h"
 #include "extensions/buildflags/buildflags.h"
 #include "extensions/common/api/chrome_url_overrides.h"
@@ -365,6 +366,7 @@
                             std::move(chrome_url_overrides))
             .Build();
 
+    PermissionsUpdater(profile_.get()).GrantActivePermissions(extension.get());
     registrar()->AddExtension(extension.get());
     EXPECT_EQ(extension,
               ExtensionUrlOverrides::GetExtensionControllingURL(
diff --git a/chrome/browser/extensions/service_worker_registration_apitest.cc b/chrome/browser/extensions/service_worker_registration_apitest.cc
index cd8962fad..4192d299 100644
--- a/chrome/browser/extensions/service_worker_registration_apitest.cc
+++ b/chrome/browser/extensions/service_worker_registration_apitest.cc
@@ -470,7 +470,7 @@
   const Extension* extension = nullptr;
   {
     ExtensionTestMessageListener listener("v1 ready");
-    extension = InstallExtension(crx_v1, 1);
+    extension = InstallExtensionWithPermissionsGranted(crx_v1, 1);
     ASSERT_TRUE(listener.WaitUntilSatisfied());
   }
 
@@ -1011,7 +1011,8 @@
   const Extension* extension_v1 = nullptr;
   {
     ExtensionTestMessageListener v1_install_listener_("v1 installed");
-    extension_v1 = InstallExtension(crx_v1_path, /*expected_change=*/1);
+    extension_v1 = InstallExtensionWithPermissionsGranted(
+        crx_v1_path, /*expected_change=*/1);
     SCOPED_TRACE("waiting for version 1 of the extension to install");
     ASSERT_TRUE(v1_install_listener_.WaitUntilSatisfied());
     ASSERT_EQ("1", extension_v1->version().GetString());
diff --git a/chrome/browser/ui/extensions/settings_overridden_params_providers_unittest.cc b/chrome/browser/ui/extensions/settings_overridden_params_providers_unittest.cc
index 8dd6191..f2bc0e2 100644
--- a/chrome/browser/ui/extensions/settings_overridden_params_providers_unittest.cc
+++ b/chrome/browser/ui/extensions/settings_overridden_params_providers_unittest.cc
@@ -15,6 +15,7 @@
 #include "chrome/test/base/search_test_utils.h"
 #include "components/search_engines/template_url_service.h"
 #include "extensions/browser/extension_registrar.h"
+#include "extensions/browser/permissions/permissions_updater.h"
 #include "extensions/browser/ui_util.h"
 #include "extensions/common/extension_builder.h"
 
@@ -54,6 +55,8 @@
                             std::move(chrome_url_overrides))
             .Build();
 
+    extensions::PermissionsUpdater(profile()).GrantActivePermissions(
+        extension.get());
     registrar()->AddExtension(extension);
     EXPECT_EQ(extension, ExtensionUrlOverrides::GetExtensionControllingURL(
                              chrome::ChromeUINewTabURLAsGURL(), profile()));
diff --git a/chrome/common/extensions/permissions/chrome_permission_message_provider.cc b/chrome/common/extensions/permissions/chrome_permission_message_provider.cc
index fe27182..0a834b1 100644
--- a/chrome/common/extensions/permissions/chrome_permission_message_provider.cc
+++ b/chrome/common/extensions/permissions/chrome_permission_message_provider.cc
@@ -186,13 +186,6 @@
   if (requested_permissions.ShouldWarnAllHosts())
     potential_total_ids.insert(APIPermissionID::kHostsAll);
 
-  // For M62, we added a new permission ID for new tab page overrides. Consider
-  // the addition of this permission to not result in a privilege increase for
-  // the time being.
-  // TODO(robertshield): Remove this once most of the population is on M62+
-  granted_ids.erase(APIPermissionID::kNewTabPageOverride);
-  potential_total_ids.erase(APIPermissionID::kNewTabPageOverride);
-
   // If all the IDs were already there, it's not a privilege increase.
   if (granted_ids.Includes(potential_total_ids))
     return false;
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/extensions/extension_loading_browsertest.cc b/chrome/browser/extensions/extension_loading_browsertest.cc
index d509ba6..4c914697 100644
--- a/chrome/browser/extensions/extension_loading_browsertest.cc
+++ b/chrome/browser/extensions/extension_loading_browsertest.cc
@@ -23,8 +23,10 @@
 #include "content/public/browser/devtools_agent_host.h"
 #include "content/public/test/browser_test.h"
 #include "content/public/test/browser_test_utils.h"
+#include "extensions/browser/disable_reason.h"
 #include "extensions/browser/event_router.h"
 #include "extensions/browser/extension_host.h"
+#include "extensions/browser/extension_prefs.h"
 #include "extensions/browser/extension_registry.h"
 #include "extensions/browser/process_manager.h"
 #include "extensions/common/manifest_handlers/background_info.h"
@@ -73,8 +75,8 @@
   extension_dir.WriteFile(FILE_PATH_LITERAL("newtab.html"),
                           "<h1>Overridden New Tab Page</h1>");
 
-  const Extension* new_tab_extension =
-      InstallExtension(extension_dir.Pack(), 1 /*new install*/);
+  const Extension* new_tab_extension = InstallExtensionWithPermissionsGranted(
+      extension_dir.Pack(), 1 /*new install*/);
   ASSERT_TRUE(new_tab_extension);
 
   // Visit the New Tab Page to get a renderer using the extension into history.
@@ -109,7 +111,7 @@
 }
 
 IN_PROC_BROWSER_TEST_F(ExtensionLoadingTest,
-                       UpgradeAddingNewTabPagePermissionNoPrompt) {
+                       UpgradeAddingNewTabPagePermissionDisablesExtension) {
   ASSERT_TRUE(embedded_test_server()->Start());
 
   TestExtensionDir extension_dir;
@@ -149,19 +151,22 @@
   extension_dir.WriteManifest(
       base::StringPrintf(kManifestTemplate, 2, kNtpOverrideString));
 
-  // Upgrade the extension, ensure that the upgrade 'worked' in the sense that
-  // the extension is still present and not disabled and that it now has the
-  // new API permission.
-  // TODO(robertshield): Update this once most of the population is on M62+
-  // and adding NTP permissions implies a permission upgrade.
-  new_tab_extension = UpdateExtension(
-      new_tab_extension->id(), extension_dir.Pack(), 0 /*expected upgrade*/);
+  std::string extension_id = new_tab_extension->id();
+
+  // Upgrade the extension, and ensure that adding NTP permissions implies a
+  // permission upgrade, so the extension is disabled.
+  UpdateExtension(extension_id, extension_dir.Pack(), -1 /*expected upgrade*/);
+
+  ExtensionRegistry* registry = ExtensionRegistry::Get(profile());
+  new_tab_extension = registry->disabled_extensions().GetByID(extension_id);
   ASSERT_NE(nullptr, new_tab_extension);
 
   EXPECT_TRUE(new_tab_extension->permissions_data()->HasAPIPermission(
       mojom::APIPermissionID::kNewTabPageOverride));
   EXPECT_THAT(new_tab_extension->version().components(),
               testing::ElementsAre(2));
+  EXPECT_TRUE(ExtensionPrefs::Get(profile())->HasDisableReason(
+      extension_id, disable_reason::DISABLE_PERMISSIONS_INCREASE));
 }
 
 // Tests the behavior described in http://crbug.com/41201916.
diff --git a/chrome/browser/extensions/extension_url_overrides_unittest.cc b/chrome/browser/extensions/extension_url_overrides_unittest.cc
index fac625b4..2f9c15d 100644
--- a/chrome/browser/extensions/extension_url_overrides_unittest.cc
+++ b/chrome/browser/extensions/extension_url_overrides_unittest.cc
@@ -26,6 +26,7 @@
 #include "extensions/browser/extension_registrar.h"
 #include "extensions/browser/extension_system.h"
 #include "extensions/browser/mock_external_provider.h"
+#include "extensions/browser/permissions/permissions_updater.h"
 #include "extensions/browser/test_extension_registry_observer.h"
 #include "extensions/buildflags/buildflags.h"
 #include "extensions/common/api/chrome_url_overrides.h"
@@ -365,6 +366,7 @@
                             std::move(chrome_url_overrides))
             .Build();
 
+    PermissionsUpdater(profile_.get()).GrantActivePermissions(extension.get());
     registrar()->AddExtension(extension.get());
     EXPECT_EQ(extension,
               ExtensionUrlOverrides::GetExtensionControllingURL(
diff --git a/chrome/browser/extensions/service_worker_registration_apitest.cc b/chrome/browser/extensions/service_worker_registration_apitest.cc
index cd8962fad..4192d299 100644
--- a/chrome/browser/extensions/service_worker_registration_apitest.cc
+++ b/chrome/browser/extensions/service_worker_registration_apitest.cc
@@ -470,7 +470,7 @@
   const Extension* extension = nullptr;
   {
     ExtensionTestMessageListener listener("v1 ready");
-    extension = InstallExtension(crx_v1, 1);
+    extension = InstallExtensionWithPermissionsGranted(crx_v1, 1);
     ASSERT_TRUE(listener.WaitUntilSatisfied());
   }
 
@@ -1011,7 +1011,8 @@
   const Extension* extension_v1 = nullptr;
   {
     ExtensionTestMessageListener v1_install_listener_("v1 installed");
-    extension_v1 = InstallExtension(crx_v1_path, /*expected_change=*/1);
+    extension_v1 = InstallExtensionWithPermissionsGranted(
+        crx_v1_path, /*expected_change=*/1);
     SCOPED_TRACE("waiting for version 1 of the extension to install");
     ASSERT_TRUE(v1_install_listener_.WaitUntilSatisfied());
     ASSERT_EQ("1", extension_v1->version().GetString());
diff --git a/chrome/browser/ui/extensions/settings_overridden_params_providers_unittest.cc b/chrome/browser/ui/extensions/settings_overridden_params_providers_unittest.cc
index 8dd6191..f2bc0e2 100644
--- a/chrome/browser/ui/extensions/settings_overridden_params_providers_unittest.cc
+++ b/chrome/browser/ui/extensions/settings_overridden_params_providers_unittest.cc
@@ -15,6 +15,7 @@
 #include "chrome/test/base/search_test_utils.h"
 #include "components/search_engines/template_url_service.h"
 #include "extensions/browser/extension_registrar.h"
+#include "extensions/browser/permissions/permissions_updater.h"
 #include "extensions/browser/ui_util.h"
 #include "extensions/common/extension_builder.h"
 
@@ -54,6 +55,8 @@
                             std::move(chrome_url_overrides))
             .Build();
 
+    extensions::PermissionsUpdater(profile()).GrantActivePermissions(
+        extension.get());
     registrar()->AddExtension(extension);
     EXPECT_EQ(extension, ExtensionUrlOverrides::GetExtensionControllingURL(
                              chrome::ChromeUINewTabURLAsGURL(), profile()));
Loading diff…

Original Bug Report

reported by [email protected]

Potential bypass of New Tab Page override warning via Web Store API logic error

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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: A logic error in the extension permission system allows a compromised Web Store renderer to suppress the mandatory New Tab Page override warning. By providing a spoofed manifest that omits the override, an attacker can trick the user into approving an installation that silently hijacks the New Tab Page. This is enabled by stale code in the permission provider that explicitly ignores New Tab Page override permissions during verification.

Affected files:

  • chrome/common/extensions/permissions/chrome_permission_message_provider.cc
  • chrome/browser/extensions/api/webstore_private/webstore_private_api.cc
  • extensions/browser/crx_installer.cc
  • extensions/browser/install_approval.cc
  • chrome/browser/ui/extensions/settings_api_bubble_helpers.cc

Estimated timestamp from git blame: 2017-07-19

Summary

A potential vulnerability exists in the Chrome extension installation flow where a compromised renderer process hosting the chromewebstore.google.com origin can suppress the ‘Replace the page you see when opening a new tab’ warning. This occurs because the browser process fails to correctly validate the actual permissions of a downloaded CRX against the manifest approved by the user, specifically regarding New Tab Page (NTP) overrides.

Technical Details

The Chrome Web Store installation flow uses the webstorePrivate.beginInstallWithManifest3 API. The vulnerability involves the following sequence of events:

  1. Spoofed Prompt: A compromised renderer calls the API with a spoofed manifest that omits the chrome_url_overrides key. The browser creates a dummy extension from this manifest to show the installation prompt. Since the key is missing, the NTP override warning is suppressed.
  2. Loose Verification: Upon user approval, the browser creates an extensions::InstallApproval with manifest_check_level set to kLoose.
  3. Validation Bypass: After downloading the real CRX (which contains the NTP override), extensions::CrxInstaller::AllowInstall (extensions/browser/crx_installer.cc) detects the manifest mismatch and calls IsPrivilegeIncrease to verify if the new manifest requires more permissions than approved.
  4. Root Cause: In chrome/common/extensions/permissions/chrome_permission_message_provider.cc, the IsAPIOrManifestPrivilegeIncrease function contains a stale workaround (Lines 193-194):
    // For M62, we added a new permission ID for new tab page overrides. Consider
    // the addition of this permission to not result in a privilege increase for
    // the time being.
    granted_ids.erase(APIPermissionID::kNewTabPageOverride);
    potential_total_ids.erase(APIPermissionID::kNewTabPageOverride);
    
    By erasing kNewTabPageOverride from the comparison sets, the browser incorrectly concludes that the addition of an NTP override is not a privilege increase.

Impact

An attacker can silently hijack a user’s New Tab Page. This is particularly effective on Linux, where the post-install ‘Extension Settings Overridden’ notification is explicitly disabled (g_ntp_post_install_ui_enabled = false in chrome/browser/ui/extensions/settings_api_bubble_helpers.cc), making the override entirely transparent to the user.

Potential Steps to Reproduce

  1. Compromise a renderer process with access to https://chromewebstore.google.com/*.
  2. Call chrome.webstorePrivate.beginInstallWithManifest3 with the ID of a legitimate NTP-overriding extension, but use a manifest string that omits the chrome_url_overrides field.
  3. Observe that the resulting install prompt does not warn the user about the NTP override.
  4. After user approval, the extension is installed and the NTP is successfully hijacked.

Remove the stale logic in chrome/common/extensions/permissions/chrome_permission_message_provider.cc that erases APIPermissionID::kNewTabPageOverride from the granted_ids and potential_total_ids sets. This will ensure that NTP overrides are treated as a privilege increase if they were not present in the user-approved manifest.

Evaluated with Chrome root at commit: 1a8d40fc44df2088d5945c0bf53584038aa1614a


Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:

  • If you are familiar with the severity guidelines, you may adjust the severity.
  • If this is a false positive, and there’s no work to be done, please close as WAI.
  • If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.

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