Overview

Medium
Severity
5.4
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUI misrepresentation in Browser
DescriptionUI misrepresentation in Browser
ComponentBrowser
Sub-componentJs_optimization
Bug ClassSpoofing
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety
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.
Tracker514074827
Fix commit6727f6d99a12 (chromium/src) +46/-0
CWECWE-451
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:N/A:L
CISA KEVNot listed
CreditedGoogle
Reported2026-05-17 (per vendor advisory)
Fix landed2026-07-28 (public git)
Disclosed2026-09-08
Patch gap42 days — fix public in source before the advisory

Background

JS optimizations page-action bubble
the omnibox popup managed by JsOptimizationsPageActionController that offers an “Allow for this site” action to re-enable V8/JavaScript optimizations for the page currently shown in the tab.
`PrimaryPageChanged()`
a content::WebContentsObserver-style callback (invoked with a content::Page&) that fires when the tab’s primary page is replaced, e.g. on a cross-document navigation.
`HostContentSettingsMap`
the per-profile store that records content-setting exceptions (here ContentSettingsType::JAVASCRIPT_OPTIMIZER), keyed by the site the setting is written for.
Late-binding action
the bubble resolves its target site from the tab’s current WebContents at click time rather than from the page it was opened for, so its effect can drift if the tab navigates while the bubble stays open.

Root Cause Analysis

Before the patch, JsOptimizationsPageActionController::PrimaryPageChanged(content::Page& page) only called UpdateIconVisibility() and left any open bubble on screen across a primary-page change. Because the bubble’s “Allow for this site” action reads the target site from the tab’s live WebContents at click time (per the commit message), a navigation to a different site while the bubble was open left a bubble that visually belonged to the original page but would apply its content-setting exception to the newly loaded site. This violates the invariant that a page-action bubble’s contents and action must stay scoped to the single page it was created for.

The fix adds a guard if (bubble_ && bubble_->GetWidget()) that calls bubble_->GetWidget()->Close() at the top of PrimaryPageChanged(), dismissing the stale bubble before UpdateIconVisibility() runs so the drifting action can no longer be confirmed against the wrong origin. The shipped browser test BubbleClosesOnNavigation confirms the intended behavior: after opening the bubble on a.com and navigating to b.com, the bubble is hidden and no CONTENT_SETTING_BLOCK-to-allow exception is written for either a.com or b.com.

Key insight
The single mistake was leaving an origin-scoped UI element alive across a navigation while its confirming action late-binds to the tab’s current WebContents; the fix closes the bubble in PrimaryPageChanged() so the action can never be applied to a page other than the one the bubble was opened for.

Attack Path

  1. Trigger the icon On an attacker-controlled site whose JavaScript optimizer is blocked, the attacker gets the user to a page where the omnibox JS-optimizations page-action icon (kJsOptimizationsIconElementId) is visible.
  2. Open the bubble The user clicks the icon and the bubble (kBubbleBodyElementId) appears, presenting “Allow for this site” scoped, in the user’s understanding, to the currently displayed origin.
  3. Navigate under the open bubble While the bubble is still open, the page navigates the tab to a different site (a primary-page change); pre-patch, PrimaryPageChanged() leaves the bubble on screen even though the tab now shows a different origin.
  4. Action drifts to the new site Because the action resolves its target from the tab’s current WebContents at click time (per the commit message; the diff itself does not show that resolution code), confirming “Allow” writes/represents the exception against the newly loaded site rather than the one the bubble was opened for, producing a UI-misrepresentation / spoofing outcome where the user’s intent is bound to the wrong origin (inferred).

Impact Assessment

The advisory classifies this as a Spoofing / UI-misrepresentation bug in the Browser process: an attacker can make the JS-optimizations bubble represent or apply its “Allow for this site” action to an origin different from the one the user believes they are acting on, by navigating the tab while the bubble is open. Preconditions are that the JS optimizer is blocked for the shown site, the user opens the bubble, and a navigation occurs before the action is confirmed. The vendor advisory rates this medium severity and does not report in-the-wild exploitation; NVD scores it CVSS 5.4. Any escalation of a mis-scoped content-setting exception into broader compromise is (inferred) rather than demonstrated by the patch.

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

Audit Directions

  • Origin-scoped bubbles surviving navigation
    Audit other page-action / omnibox bubble controllers that observe WebContents for whether they close their bubble in PrimaryPageChanged() (or an equivalent navigation callback); the commit cites SendTabToSelfBubbleController as the pattern to match.
  • Late-binding of action target
    Look for UI actions that resolve their target site/origin from the current WebContents at confirm/click time rather than capturing it when the UI was created, since an intervening navigation can silently redirect the effect.
  • Content-setting writes from persistent UI
    Review flows that write HostContentSettingsMap exceptions from long-lived UI surfaces and confirm the write key is bound to the page the UI was opened for, not to whatever the tab happens to display when the user clicks.
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/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();
 }
 
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…

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