Medium watch ✔︎ verified CVSS 4.3 chrome Policy Bypass 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
4.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient policy enforcement in Extensions
DescriptionInsufficient policy enforcement in Extensions
ComponentExtensions
Sub-componentExtensions
Bug ClassPolicy Bypass
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety — upheld by independent review by AI
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Independent review by AIConfirmed
Reviewer notes: The 'permissions: ["tabs"]' v2.0 test detail is not in the shown (truncated) diff. The baseline grant runs whenever !is_extension_loaded for external installs, with a TODO, not just at first install as implied.
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker435980394
Fix commitb846067dcb8e (chromium/src) +187/-33
CWECWE-451
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:L/A:N
CISA KEVNot listed
Creditedportsniffer443
Reported2025-08-03 · bounty $2000 (per vendor advisory)
Fix landed2026-01-14 (public git)
Disclosed2026-03-10
Patch gap55 days — fix public in source before the advisory

Background

External extension install
an extension whose ManifestLocation is one of the external locations (e.g. kExternalPref, as used by the regression test’s MockExternalProvider), i.e. installed by an external provider such as a preferences file or registry entry, not through the normal Web Store install flow (kInternal).
Privilege-increase check
the logic that compares an extension’s previously granted permissions (GetGrantedPermissions unioned with GetRuntimeGrantedPermissions) against its current active_permissions() using PermissionMessageProvider::IsPrivilegeIncrease, so that an update requesting new warning-bearing permissions is disabled until the user approves.
`DISABLE_PERMISSIONS_INCREASE`
the disable reason applied to an extension whose update asks for more privileges than the user has granted; the new browser test expects this reason after an external extension update adds the tabs permission.
Trusted locations
component (IsComponentLocation), policy (IsPolicyLocation) and unpacked (IsUnpackedLocation) installs, which the patched code treats as exempt from the user-approval check.

Root Cause Analysis

In chrome/browser/extensions/chrome_extension_registrar_delegate.cc, the permission-increase block (the enclosing function name is not shown in the hunk) only ran the comparison when extension->location() == ManifestLocation::kInternal && !auto_grant_permission. For every other location, including externally installed extensions, is_privilege_increase stayed false and no comparison was made. So an external extension could ship an update that requested new, warning-bearing permissions (the test adds "permissions": ["tabs"] in version 2.0) and stay enabled with them, without the user being asked.

The patch changes the gate to an allowlist of trusted locations: it skips the check only for component, policy and unpacked installs (or when auto_grant_permission is set). Every other location, external ones included, now goes through IsPrivilegeIncrease. For an external extension that is not yet loaded (!is_extension_loaded && is_external_install), the code calls PermissionsUpdater::GrantActivePermissions to record a baseline granted set, so later updates have something to be compared against.

Key insight
The privilege-increase check was opt-in by location (kInternal only), so external installs silently skipped user re-approval on permission growth. The fix makes it opt-out (only component/policy/unpacked are exempt) and seeds a granted-permission baseline for external extensions at first load.

Attack Path

  1. External extension present an extension is installed through an external provider (an external location such as kExternalPref) and is enabled with an initial, low-privilege manifest.
  2. Permission-expanding update the extension’s author, or whoever controls its update source, publishes a new version with the same ID that requests additional permissions (the test uses tabs).
  3. Check skipped on load when the update is installed, the location is not kInternal, so the pre-patch code never computed is_privilege_increase and the extension was not disabled.
  4. Silent privilege gain the updated extension runs with the new permissions and no DISABLE_PERMISSIONS_INCREASE prompt, so the user never consents to the expanded access (the specific data then accessed is (inferred)).

Impact Assessment

An extension delivered through an external install location can gain additional API permissions on update without the user re-approving them. The new permissions are exercised in the extension’s own (browser-privileged extension) context. Preconditions: the extension must already be externally installed and enabled, and the attacker must control a later version of it (a malicious or compromised extension developer or update channel). Ordinary web content cannot trigger this. The vendor rates it medium severity (“Insufficient policy enforcement in Extensions”).

Changed Functions

FunctionChangeNotes
if
chrome/browser/extensions/chrome_extension_registrar_delegate.cc
modified
ExternalExtensionInstallBrowserTest
chrome/browser/extensions/external_extension_install_browsertest.cc
modified

Files Changed

  • chrome/browser/extensions/chrome_extension_registrar_delegate.cc
  • chrome/browser/extensions/external_extension_install_browsertest.cc

Audit Directions

  • Location-allowlisted security checks
    look for other extension->location() == kInternal (or similar single-location) conditions that gate security decisions, such as install verification, permission withholding or disable reasons. Check whether external, kExternalPrefDownload or kExternalRegistry installs fall through without enforcement.
  • Missing granted-permission baselines
    the TODO(crbug.com/435980394) notes that external installs do not get permissions granted at install time. Audit code that reads GetGrantedPermissions for extensions that may never have had a granted set recorded, because an empty or absent baseline can make comparisons pass or fail incorrectly.
  • Update paths vs. install paths
    compare the checks applied when an extension is first installed with those applied when it is updated, for each install source (external providers, sideloading, policy), and look for flows where only one of the two runs the user-consent check.
From b846067dcb8e1024449a9682761a4867b85140a7 Mon Sep 17 00:00:00 2001
From: Solomon Kinard <[email protected]>
Date: Wed, 14 Jan 2026 14:49:06 -0800
Subject: [PATCH] Extensions: Permissions: Disable external extensions on increase

Disable externally installed extensions if their permissions have
increased.

Bug: 435980394
Change-Id: I39ab160ecade0cbd582d65801a3f47b72976ce7d
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7265369
Reviewed-by: Devlin Cronin <[email protected]>
Commit-Queue: Solomon Kinard <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1569353}
---

diff --git a/chrome/browser/extensions/chrome_extension_registrar_delegate.cc b/chrome/browser/extensions/chrome_extension_registrar_delegate.cc
index bc49001..3c10b340 100644
--- a/chrome/browser/extensions/chrome_extension_registrar_delegate.cc
+++ b/chrome/browser/extensions/chrome_extension_registrar_delegate.cc
@@ -23,6 +23,7 @@
 #include "chrome/browser/extensions/external_install_manager.h"
 #include "chrome/browser/extensions/install_verifier_factory.h"
 #include "chrome/browser/extensions/installed_loader.h"
+#include "chrome/browser/extensions/managed_installation_mode.h"
 #include "chrome/browser/extensions/profile_util.h"
 #include "chrome/browser/extensions/updater/extension_updater.h"
 #include "chrome/browser/profiles/profile.h"
@@ -45,6 +46,7 @@
 #include "extensions/buildflags/buildflags.h"
 #include "extensions/common/crash_keys.h"
 #include "extensions/common/extension.h"
+#include "extensions/common/manifest_constants.h"
 #include "extensions/common/manifest_handlers/incognito_info.h"
 #include "extensions/common/manifest_handlers/shared_module_info.h"
 #include "extensions/common/mojom/manifest.mojom-shared.h"
@@ -517,42 +519,51 @@
   }
 
   bool is_privilege_increase = false;
-  // We only need to compare the granted permissions to the current permissions
-  // if the extension has not been auto-granted its permissions above and is
-  // installed internally.
-  if (extension->location() == ManifestLocation::kInternal &&
-      !auto_grant_permission) {
-    // Add all the recognized permissions if the granted permissions list
-    // hasn't been initialized yet.
-    std::unique_ptr<const PermissionSet> granted_permissions =
-        extension_prefs_->GetGrantedPermissions(extension->id());
-    CHECK(granted_permissions.get());
-    // We check the union of both granted permissions and runtime granted
-    // permissions as it is possible for permissions which were withheld during
-    // installation to have never entered the granted set, but to have later
-    // been granted as runtime permissions.
-    std::unique_ptr<const PermissionSet> runtime_granted_permissions =
-        extension_prefs_->GetRuntimeGrantedPermissions(extension->id());
-    std::unique_ptr<const PermissionSet> total_permissions =
-        PermissionSet::CreateUnion(*granted_permissions,
-                                   *runtime_granted_permissions);
 
-    // Here, we check if an extension's privileges have increased in a manner
-    // that requires the user's approval. This could occur because the browser
-    // upgraded and recognized additional privileges, or an extension upgrades
-    // to a version that requires additional privileges.
-    is_privilege_increase =
-        PermissionMessageProvider::Get()->IsPrivilegeIncrease(
-            *total_permissions,
-            extension->permissions_data()->active_permissions(),
-            extension->GetType());
+  // Identify extensions from inherently trusted locations.
+  bool is_trusted_location =
+      Manifest::IsComponentLocation(extension->location()) ||
+      Manifest::IsPolicyLocation(extension->location()) ||
+      Manifest::IsUnpackedLocation(extension->location());
 
-    // If there was no privilege increase, the extension might still have new
-    // permissions (which either don't generate a warning message, or whose
-    // warning messages are suppressed by existing permissions). Grant the new
-    // permissions.
-    if (!is_privilege_increase) {
+  // Verify privilege increases for non-trusted and non-auto-granted extensions.
+  if (!is_trusted_location && !auto_grant_permission) {
+    bool is_external_install =
+        Manifest::IsExternalLocation(extension->location());
+    if (!is_extension_loaded && is_external_install) {
+      // Grant initial permissions to establish a baseline for update checks.
+      // TODO(crbug.com/435980394): Grant for this extension type on install.
       PermissionsUpdater(profile_).GrantActivePermissions(extension);
+    } else {
+      // Add all the recognized permissions if the granted permissions list
+      // hasn't been initialized yet. Compare requested permissions against the
+      // existing granted set to detect a privilege increase.
+      std::unique_ptr<const PermissionSet> granted_permissions =
+          extension_prefs_->GetGrantedPermissions(extension->id());
+      CHECK(granted_permissions.get());
+      std::unique_ptr<const PermissionSet> runtime_granted_permissions =
+          extension_prefs_->GetRuntimeGrantedPermissions(extension->id());
+      std::unique_ptr<const PermissionSet> total_permissions =
+          PermissionSet::CreateUnion(*granted_permissions,
+                                     *runtime_granted_permissions);
+
+      // Check if an extension's privileges have increased in a manner that
+      // requires the user's approval. This could occur because the browser
+      // upgraded and recognized additional privileges, or an extension upgrades
+      // to a version that requires additional privileges.
+      is_privilege_increase =
+          PermissionMessageProvider::Get()->IsPrivilegeIncrease(
+              *total_permissions,
+              extension->permissions_data()->active_permissions(),
+              extension->GetType());
+
+      // If there was no privilege increase, the extension might still have new
+      // permissions (which either don't generate a warning message, or whose
+      // warning messages are suppressed by existing permissions). Grant the new
+      // permissions.
+      if (!is_privilege_increase) {
+        PermissionsUpdater(profile_).GrantActivePermissions(extension);
+      }
     }
   }
 
diff --git a/chrome/browser/extensions/external_extension_install_browsertest.cc b/chrome/browser/extensions/external_extension_install_browsertest.cc
new file mode 100644
index 0000000..23ff933
--- /dev/null
+++ b/chrome/browser/extensions/external_extension_install_browsertest.cc
@@ -0,0 +1,142 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "base/files/file_util.h"
+#include "base/one_shot_event.h"
+#include "base/strings/stringprintf.h"
+#include "base/test/bind.h"
+#include "base/threading/thread_restrictions.h"
+#include "chrome/browser/extensions/extension_browsertest.h"
+#include "chrome/browser/extensions/extension_management_internal.h"
+#include "chrome/browser/extensions/external_provider_manager.h"
+#include "chrome/browser/extensions/mv2_experiment_stage.h"
+#include "chrome/browser/profiles/profile.h"
+#include "components/crx_file/id_util.h"
+#include "content/public/test/browser_test.h"
+#include "extensions/browser/extension_prefs.h"
+#include "extensions/browser/extension_registrar.h"
+#include "extensions/browser/extension_registry.h"
+#include "extensions/browser/extension_system.h"
+#include "extensions/browser/mock_external_provider.h"
+#include "extensions/browser/pref_names.h"
+#include "extensions/browser/test_extension_registry_observer.h"
+#include "extensions/browser/unpacked_installer.h"
+#include "extensions/common/extension.h"
+#include "extensions/common/extension_features.h"
+#include "extensions/common/feature_switch.h"
+#include "extensions/common/mojom/manifest.mojom.h"
+#include "extensions/test/test_extension_dir.h"
+#include "testing/gmock/include/gmock/gmock.h"
+
+namespace extensions {
+
+class ExternalExtensionInstallBrowserTest : public ExtensionBrowserTest {
+ public:
+  ExternalExtensionInstallBrowserTest() = default;
+  ~ExternalExtensionInstallBrowserTest() override = default;
+
+  // Reads private key from `private_key_path` and generates extension id using
+  // it.
+  std::string GetExtensionIdFromPrivateKeyFile(
+      const base::FilePath& private_key_path) {
+    base::ScopedAllowBlockingForTesting allow_file_io_in_scope;
+    std::string private_key_contents;
+    EXPECT_TRUE(
+        base::ReadFileToString(private_key_path, &private_key_contents));
+    std::string private_key_bytes;
+    EXPECT_TRUE(
+        Extension::ParsePEMKeyBytes(private_key_contents, &private_key_bytes));
+    auto signing_key = crypto::keypair::PrivateKey::FromPrivateKeyInfo(
+        base::as_byte_span(private_key_bytes));
+    std::vector<uint8_t> public_key = signing_key->ToSubjectPublicKeyInfo();
+    return crx_file::id_util::GenerateId(public_key);
+  }
+};
+
+// Verify that an externally installed extension is disabled on update if the
+// permissions have increased.
+IN_PROC_BROWSER_TEST_F(ExternalExtensionInstallBrowserTest,
+                       AddPermissionOnUpdate) {
+  // Setup.
+  TestExtensionDir test_dir;
+  std::string manifest;
+
+  // Write manifest.
+  manifest = R"({
+      "name": "Test",
+      "version": "1.0",
+      "manifest_version": 3
+    })";
+  test_dir.WriteManifest(manifest);
+
+  // Pack the v1 extension. This generates the CRX and a PEM (private key) file.
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/extensions/external_extension_install_browsertest.cc b/chrome/browser/extensions/external_extension_install_browsertest.cc
new file mode 100644
index 0000000..23ff933
--- /dev/null
+++ b/chrome/browser/extensions/external_extension_install_browsertest.cc
@@ -0,0 +1,142 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "base/files/file_util.h"
+#include "base/one_shot_event.h"
+#include "base/strings/stringprintf.h"
+#include "base/test/bind.h"
+#include "base/threading/thread_restrictions.h"
+#include "chrome/browser/extensions/extension_browsertest.h"
+#include "chrome/browser/extensions/extension_management_internal.h"
+#include "chrome/browser/extensions/external_provider_manager.h"
+#include "chrome/browser/extensions/mv2_experiment_stage.h"
+#include "chrome/browser/profiles/profile.h"
+#include "components/crx_file/id_util.h"
+#include "content/public/test/browser_test.h"
+#include "extensions/browser/extension_prefs.h"
+#include "extensions/browser/extension_registrar.h"
+#include "extensions/browser/extension_registry.h"
+#include "extensions/browser/extension_system.h"
+#include "extensions/browser/mock_external_provider.h"
+#include "extensions/browser/pref_names.h"
+#include "extensions/browser/test_extension_registry_observer.h"
+#include "extensions/browser/unpacked_installer.h"
+#include "extensions/common/extension.h"
+#include "extensions/common/extension_features.h"
+#include "extensions/common/feature_switch.h"
+#include "extensions/common/mojom/manifest.mojom.h"
+#include "extensions/test/test_extension_dir.h"
+#include "testing/gmock/include/gmock/gmock.h"
+
+namespace extensions {
+
+class ExternalExtensionInstallBrowserTest : public ExtensionBrowserTest {
+ public:
+  ExternalExtensionInstallBrowserTest() = default;
+  ~ExternalExtensionInstallBrowserTest() override = default;
+
+  // Reads private key from `private_key_path` and generates extension id using
+  // it.
+  std::string GetExtensionIdFromPrivateKeyFile(
+      const base::FilePath& private_key_path) {
+    base::ScopedAllowBlockingForTesting allow_file_io_in_scope;
+    std::string private_key_contents;
+    EXPECT_TRUE(
+        base::ReadFileToString(private_key_path, &private_key_contents));
+    std::string private_key_bytes;
+    EXPECT_TRUE(
+        Extension::ParsePEMKeyBytes(private_key_contents, &private_key_bytes));
+    auto signing_key = crypto::keypair::PrivateKey::FromPrivateKeyInfo(
+        base::as_byte_span(private_key_bytes));
+    std::vector<uint8_t> public_key = signing_key->ToSubjectPublicKeyInfo();
+    return crx_file::id_util::GenerateId(public_key);
+  }
+};
+
+// Verify that an externally installed extension is disabled on update if the
+// permissions have increased.
+IN_PROC_BROWSER_TEST_F(ExternalExtensionInstallBrowserTest,
+                       AddPermissionOnUpdate) {
+  // Setup.
+  TestExtensionDir test_dir;
+  std::string manifest;
+
+  // Write manifest.
+  manifest = R"({
+      "name": "Test",
+      "version": "1.0",
+      "manifest_version": 3
+    })";
+  test_dir.WriteManifest(manifest);
+
+  // Pack the v1 extension. This generates the CRX and a PEM (private key) file.
+  base::FilePath v1_crx_path = PackExtension(test_dir.UnpackedPath());
+  ASSERT_FALSE(v1_crx_path.empty());
+
+  // Locate the generated pem file (temp.pem) in the same directory.
+  base::FilePath pem_path = v1_crx_path.DirName().AppendASCII("temp.pem");
+  {
+    base::ScopedAllowBlockingForTesting allow_file_io_in_scope;
+    ASSERT_TRUE(base::PathExists(pem_path));
+  }
+
+  // Read the actual RSA key content, ensuring a perfect match.
+  const std::string extension_id = GetExtensionIdFromPrivateKeyFile(pem_path);
+
+  // Instantiate the necessary providers for an external extension installation.
+  ExternalProviderManager* external_provider_manager =
+      ExternalProviderManager::Get(profile());
+  TestExtensionRegistryObserver observer(extension_registry());
+  auto provider = std::make_unique<MockExternalProvider>(
+      external_provider_manager, mojom::ManifestLocation::kExternalPref);
+
+  // Store the raw pointer before moving the unique_ptr, for later reuse.
+  MockExternalProvider* provider_ptr = provider.get();
+
+  // Install the external extension.
+  provider->UpdateOrAddExtension(extension_id, "1.0", v1_crx_path);
+  external_provider_manager->AddProviderForTesting(std::move(provider));
+  external_provider_manager->CheckForExternalUpdates();
+
+  // Verify that the extension is installed.
+  auto extension = observer.WaitForExtensionInstalled();
+  EXPECT_EQ(extension->id(), extension_id);
+
+  // Verify that the extension is enabled.
+  ASSERT_TRUE(extension_registrar()->IsExtensionEnabled(extension_id));
+
+  // Update the extension by increasing the permission and bumping the version.
+  manifest = R"({
+      "name": "Test",
+      "version": "2.0",
+      "manifest_version": 3,
+      "permissions": ["tabs"]
+    })";
+  test_dir.WriteManifest(manifest);
+
+  // Define a new path for the V2 CRX to avoid overwriting V1 while it might be
+  // in use.
+  base::FilePath v2_crx_path = v1_crx_path.DirName().AppendASCII("v2.crx");
+
+  // Keep the same extension id by sharing the pem between versions.
+  PackExtensionWithOptions(test_dir.UnpackedPath(), v2_crx_path, pem_path,
+                           /*pem_out_path=*/base::FilePath(),
+                           extensions::ExtensionCreator::kOverwriteCRX);
+
+  // Install the updated extension.
+  provider_ptr->UpdateOrAddExtension(extension_id, "2.0", v2_crx_path);
+  ExternalProviderManager::Get(profile())->CheckForExternalUpdates();
+
+  // Verify the extension version bump.
+  extension = observer.WaitForExtensionInstalled();
+  EXPECT_EQ(extension->version().GetString(), "2.0");
+
+  // Verify that the permission increase does not enable the updated extension.
+  ASSERT_FALSE(extension_registrar()->IsExtensionEnabled(extension_id));
+  EXPECT_THAT(
+      ExtensionPrefs::Get(profile())->GetDisableReasons(extension_id),
+      testing::ElementsAre(disable_reason::DISABLE_PERMISSIONS_INCREASE));
+}
+
+}  // namespace extensions
diff --git a/chrome/test/BUILD.gn b/chrome/test/BUILD.gn
index 496df0fd..d586627ce 100644
--- a/chrome/test/BUILD.gn
+++ b/chrome/test/BUILD.gn
@@ -4795,6 +4795,7 @@
         "../browser/extensions/extension_websocket_apitest.cc",
         "../browser/extensions/extension_webui_apitest.cc",
         "../browser/extensions/extensions_disabled_browsertest.cc",
+        "../browser/extensions/external_extension_install_browsertest.cc",
         "../browser/extensions/external_install_error_browsertest.cc",
         "../browser/extensions/feature_provider_browsertest.cc",
         "../browser/extensions/fetch_apitest.cc",
Loading diff…

Original Bug Report

reported by [email protected]

Third party installed extensions can silently increase permissions on update

Vulnerability Details

Extensions on Chrome are very powerful. They can control many aspects of the browser including (but not limited to) accessing user data, downloading files on user’s machine, modifying the webpages visited by the user. Whenever a user installs an extension from Chrome Web Store, they see a list of permissions which the extension asks for. The extension can only be installed if the user grants those permissions.

When a new version of the extension asks for more permissions than what the user originally accepted, Chrome disables the extension and notifies the user. The updated permissions are shown to the user and the extension is only enabled when the user accepts the new permissions.

In addition to the user themselves installing an extension from Chrome Web Store, third party apps can also install extensions on Chrome. This is done by adding a registry entry on Windows or via a preferences JSON file on macOS. Extensions added by this mechanism are disabled initially. User must accept their permissions to enable them. However, once they are enabled, Chrome doesn’t disable them if a new extension update increases its permissions. The new update can have significantly different permissions than what the user allowed. The permissions are silently granted and the user doesn’t even know that the extension can do more things now (possibly malicious).

Version

Chrome Version: 138.0.7204.184 - Stable.

Operating System: Tested on macOS, but should repro on all platforms.

Reproduction Case

  1. Publish an extension on Chrome Web Store.
  2. Install it via a preferences file on macOS.
  3. Extension will be disabled initially. Enable it by accepting the permissions.
  4. Publish an update to the extension on Chrome Web Store. The update should have more permissions.
  5. Restart Chrome and wait for extension updates to finish.

Observe that the extension is still enabled but has increased permissions.

Since this reproduction is difficult (because it requires publishing to store), I am attaching videos of the repro case.

  1. The first video shows the case where the extension is installed normally. It gets disabled on update due to permissions increase.
  2. The second video shows the same extension installed via preferences file (macOS). It can be observed that it silently increases permissions on update and remains enabled.

Code references

  1. Permissions increase check is only done for extensions with kInternal manifest location.
  2. This wasn’t always the case. When the check was originally added, it also checked for kExternal manifest locations. A later commit changed to code to just check for kInternal location, probably unintentionally.

Original commit (see ExtensionsService::OnExtensionsLoaded method): https://codereview.chromium.org/165414/diff/3007/chrome/browser/extensions/extensions_service.cc

The commit which probably introduced the regression (see ExtensionsService::OnExtensionsLoaded method): https://codereview.chromium.org/4687005/diff/73001/chrome/browser/extensions/extensions_service.cc

Potential fix

ChromeExtensionRegistrarDelegate::CheckPermissionsIncrease only checks for kInternal manifest location. It should also check for external manifest locations (but still exclude policy installs and component installs).

 if (extension->location() == ManifestLocation::kInternal &&
     !auto_grant_permission) {
// ...

The above code should be changed to something like this

bool is_trusted_location = Manifest::IsComponentLocation(extension->location()) ||
                           Manifest::IsPolicyLocation(extension->location())
                           Manifest::IsUnpackedLocation(extension->location());
if (!is_trusted_location && !auto_grant_permission) {
// ...
View on issue tracker