Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUI misrepresentation in Browser
DescriptionUI misrepresentation in Browser
ComponentBrowser
Bug ClassLogic Error
Tracker514074827
Fix commit6727f6d99a12 (chromium/src) +46/-0
CISA KEVNot listed
CreditedGoogle
Disclosed2026-09-08

Changed Functions

FunctionChangeNotes
JavascriptOptimizerBubbleBrowserTest_WithPolicy
chrome/browser/renderer_host/javascript_optimizer_feature_browsertest.cc
modified

Files Changed

  • chrome/browser/renderer_host/javascript_optimizer_feature_browsertest.cc
  • chrome/browser/ui/views/js_optimization/js_optimizations_page_action_controller.cc
From 6727f6d99a12e9120ce67f7489c0dec7687e50be Mon Sep 17 00:00:00 2001
From: Javier Castro <[email protected]>
Date: Mon, 27 Jul 2026 23:40:21 -0700
Subject: [PATCH] [js_optimization] Close bubble when the primary page changes

The JS optimizations page-action bubble's "Allow for this site" action
resolves the target site from the tab's current WebContents at click
time. If the tab navigates while the bubble is open, the action applies
to the new site rather than the site the bubble was opened for.

Close the bubble in PrimaryPageChanged() so its contents and action stay
scoped to the page they were created for. This matches other page-action
bubbles such as SendTabToSelfBubbleController.

Add a browser test that opens the bubble, navigates cross-site, and
verifies the bubble is dismissed and no content-setting exception is
written.

CL Authored by Gemini based on a patch that was attached to
crbug.com/514074827

Internal review:
https://chrome-internal-review.googlesource.com/c/chrome/experimental/chromium/src/+/9591015

TAG=agy
CONV=a9911cd9-ae90-462d-8bbb-24c7c8891b72

Fixed: 514074827
Change-Id: Ie82b2945afdd12a4c6ae55fe71256953e75ede73
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8157362
Auto-Submit: Javier Castro <[email protected]>
Reviewed-by: Varun Khaneja <[email protected]>
Commit-Queue: Varun Khaneja <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1669240}
---

diff --git a/chrome/browser/renderer_host/javascript_optimizer_feature_browsertest.cc b/chrome/browser/renderer_host/javascript_optimizer_feature_browsertest.cc
index 92d83db..a66e87ee 100644
--- a/chrome/browser/renderer_host/javascript_optimizer_feature_browsertest.cc
+++ b/chrome/browser/renderer_host/javascript_optimizer_feature_browsertest.cc
@@ -1583,6 +1583,47 @@
   }));
 }
 
+IN_PROC_BROWSER_TEST_F(JavascriptOptimizerBubbleBrowserTest,
+                       BubbleClosesOnNavigation) {
+  auto* map = HostContentSettingsMapFactory::GetForProfile(profile());
+  map->SetDefaultContentSetting(ContentSettingsType::JAVASCRIPT_OPTIMIZER,
+                                ContentSetting::CONTENT_SETTING_BLOCK);
+
+  ASSERT_TRUE(content::NavigateToURL(
+      web_contents(),
+      embedded_https_test_server().GetURL("a.com", "/simple.html")));
+  ASSERT_TRUE(AreV8OptimizationsDisabledOnActiveWebContents());
+  ASSERT_TRUE(IsOmnibarIconVisible());
+
+  // Click on icon.
+  RunTestSequence(PressButton(kJsOptimizationsIconElementId));
+  // Assert that bubble is visible.
+  RunTestSequence(
+      WaitForShow(JsOptimizationsPageActionController::kBubbleBodyElementId));
+  EXPECT_TRUE(IsBubbleVisible());
+
+  // Navigate to a different site while the bubble is open.
+  ASSERT_TRUE(content::NavigateToURL(
+      web_contents(),
+      embedded_https_test_server().GetURL("b.com", "/simple.html")));
+  ASSERT_TRUE(AreV8OptimizationsDisabledOnActiveWebContents());
+
+  // The bubble was opened for the previous site, so it should now be closed.
+  RunTestSequence(
+      WaitForHide(JsOptimizationsPageActionController::kBubbleBodyElementId));
+  EXPECT_FALSE(IsBubbleVisible());
+
+  // No allow exception was written for either site.
+  EXPECT_EQ(ContentSetting::CONTENT_SETTING_BLOCK,
+            map->GetContentSetting(
+                embedded_https_test_server().GetURL("a.com", "/"), GURL(),
+                ContentSettingsType::JAVASCRIPT_OPTIMIZER));
+  EXPECT_EQ(ContentSetting::CONTENT_SETTING_BLOCK,
+            map->GetContentSetting(
+                embedded_https_test_server().GetURL("b.com", "/"), GURL(),
+                ContentSettingsType::JAVASCRIPT_OPTIMIZER));
+}
+
 // JS optimizations disabled by enterprise policy.
 class JavascriptOptimizerBubbleBrowserTest_WithPolicy
     : public JavascriptOptimizerBubbleBrowserTest {};
diff --git a/chrome/browser/ui/views/js_optimization/js_optimizations_page_action_controller.cc b/chrome/browser/ui/views/js_optimization/js_optimizations_page_action_controller.cc
index 839a6c6..55cbd8a 100644
--- a/chrome/browser/ui/views/js_optimization/js_optimizations_page_action_controller.cc
+++ b/chrome/browser/ui/views/js_optimization/js_optimizations_page_action_controller.cc
@@ -58,6 +58,11 @@
 
 void JsOptimizationsPageActionController::PrimaryPageChanged(
     content::Page& page) {
+  // The bubble's contents and action are scoped to the page it was opened for,
+  // so dismiss it when the primary page changes.
+  if (bubble_ && bubble_->GetWidget()) {
+    bubble_->GetWidget()->Close();
+  }
   UpdateIconVisibility();
 }
 
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/renderer_host/javascript_optimizer_feature_browsertest.cc b/chrome/browser/renderer_host/javascript_optimizer_feature_browsertest.cc
index 92d83db..a66e87ee 100644
--- a/chrome/browser/renderer_host/javascript_optimizer_feature_browsertest.cc
+++ b/chrome/browser/renderer_host/javascript_optimizer_feature_browsertest.cc
@@ -1583,6 +1583,47 @@
   }));
 }
 
+IN_PROC_BROWSER_TEST_F(JavascriptOptimizerBubbleBrowserTest,
+                       BubbleClosesOnNavigation) {
+  auto* map = HostContentSettingsMapFactory::GetForProfile(profile());
+  map->SetDefaultContentSetting(ContentSettingsType::JAVASCRIPT_OPTIMIZER,
+                                ContentSetting::CONTENT_SETTING_BLOCK);
+
+  ASSERT_TRUE(content::NavigateToURL(
+      web_contents(),
+      embedded_https_test_server().GetURL("a.com", "/simple.html")));
+  ASSERT_TRUE(AreV8OptimizationsDisabledOnActiveWebContents());
+  ASSERT_TRUE(IsOmnibarIconVisible());
+
+  // Click on icon.
+  RunTestSequence(PressButton(kJsOptimizationsIconElementId));
+  // Assert that bubble is visible.
+  RunTestSequence(
+      WaitForShow(JsOptimizationsPageActionController::kBubbleBodyElementId));
+  EXPECT_TRUE(IsBubbleVisible());
+
+  // Navigate to a different site while the bubble is open.
+  ASSERT_TRUE(content::NavigateToURL(
+      web_contents(),
+      embedded_https_test_server().GetURL("b.com", "/simple.html")));
+  ASSERT_TRUE(AreV8OptimizationsDisabledOnActiveWebContents());
+
+  // The bubble was opened for the previous site, so it should now be closed.
+  RunTestSequence(
+      WaitForHide(JsOptimizationsPageActionController::kBubbleBodyElementId));
+  EXPECT_FALSE(IsBubbleVisible());
+
+  // No allow exception was written for either site.
+  EXPECT_EQ(ContentSetting::CONTENT_SETTING_BLOCK,
+            map->GetContentSetting(
+                embedded_https_test_server().GetURL("a.com", "/"), GURL(),
+                ContentSettingsType::JAVASCRIPT_OPTIMIZER));
+  EXPECT_EQ(ContentSetting::CONTENT_SETTING_BLOCK,
+            map->GetContentSetting(
+                embedded_https_test_server().GetURL("b.com", "/"), GURL(),
+                ContentSettingsType::JAVASCRIPT_OPTIMIZER));
+}
+
 // JS optimizations disabled by enterprise policy.
 class JavascriptOptimizerBubbleBrowserTest_WithPolicy
     : public JavascriptOptimizerBubbleBrowserTest {};
Loading diff…

Original Bug Report

reported by [email protected]

Potential stale UI in JsOptimizations bubble allows incorrect origin permission grant

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: The JavaScript optimizations page-action bubble persists across cross-origin navigations, which may allow a user to inadvertently grant V8 JIT permissions to an unintended site. This occurrs because the controller fails to close the bubble upon navigation and resolves the target origin only at the moment the ‘Allow’ button is clicked.

Affected files:

  • chrome/browser/ui/views/js_optimization/js_optimizations_page_action_controller.cc
  • chrome/browser/site_protection/site_familiarity_utils.cc

Estimated timestamp from git blame: 2025-11-26

Description

A potential logic vulnerability exists in JsOptimizationsPageActionController where the page-action bubble (the ‘JavaScript optimization is off’ dialog) persists across cross-origin navigations. Because the controller does not close the bubble upon navigation and resolves the target origin only when the ‘Allow’ button is clicked, an attacker could potentially trick a user into disabling JIT-hardening for an unintended origin.

Technical Details

The JsOptimizationsPageActionController is a tab-scoped feature that monitors site familiarity to determine if V8 optimizations should be disabled as a security mitigation (JIT hardening for unfamiliar sites). When optimizations are disabled, a chip and bubble are shown in the omnibox.

  1. Persistence across Navigation: In JsOptimizationsPageActionController::PrimaryPageChanged (lines 61-64), the controller calls UpdateIconVisibility() but does not close the active bubble_ widget. Since the bubble is managed by the controller and not automatically dismissed by the navigation framework, it remains visible to the user even after the site origin changes.

  2. Late Origin Binding: The bubble’s OK button is bound to EnableV8Optimizations() (lines 114-117). When clicked, this function calls site_protection::EnableV8Optimizations(web_contents()) (line 143).

  3. Incorrect Write Target: The helper function site_protection::EnableV8Optimizations (in chrome/browser/site_protection/site_familiarity_utils.cc:156-176) retrieves the current site URL via web_contents()->GetSiteInstance()->GetSecurityPrincipal().GetDeprecatedSiteURL(). If a navigation occurred after the bubble was opened but before the button was clicked, the CONTENT_SETTING_ALLOW for JAVASCRIPT_OPTIMIZER is persisted for the post-navigation site’s origin.

The bubble strings are origin-agnostic (e.g., ‘JavaScript optimization is off’, ‘Allow for this site’), so the user has no visual indication that the target of the permission grant has changed due to the background navigation.

Potential Attack Scenario

Note: These are suggested steps based on code analysis; our tooling has not yet executed a proof of concept.

  1. The user navigates to an unfamiliar origin (e.g., https://attacker-a.test/). The ‘JavaScript optimizations disabled’ chip appears in the omnibox.
  2. The user clicks the chip to open the bubble.
  3. While the bubble is open, the attacker page executes a navigation (e.g., location.replace('https://attacker-b.test/')) to a different unfamiliar origin.
  4. The bubble remains open despite the navigation. If the user clicks ‘Allow for this site’, the permission is granted to https://attacker-b.test/ instead of the original site.

Impact

This issue could allow an attacker to bypass the site-protection JIT-hardening defense-in-depth mitigation for an arbitrary origin. While it does not grant direct access to sensitive data, it enables an attacker to permanently exempt a target origin from JIT-hardening protections by exploiting a stale UI state. The impact is primarily on users with Enhanced Safe Browsing enabled.

  1. Modify JsOptimizationsPageActionController::PrimaryPageChanged to close the active bubble_ if it exists. This ensures the dialog does not persist across navigations.
  2. When creating the bubble in CreateBubble, the current site URL should be snapshotted and passed to the EnableV8Optimizations callback, ensuring the permission is granted to the origin the user actually intended to authorize.

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