CVE-2026-87496
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
JavascriptOptimizerBubbleBrowserTest_WithPolicychrome/browser/renderer_host/javascript_optimizer_feature_browsertest.cc |
modified |
Files Changed
chrome/browser/renderer_host/javascript_optimizer_feature_browsertest.ccchrome/browser/ui/views/js_optimization/js_optimizations_page_action_controller.cc
Patch
From 6727f6d99a12e9120ce67f7489c0dec7687e50be Mon Sep 17 00:00:00 2001
From: Javier Castro <jacastro@chromium.org>
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 <jacastro@chromium.org>
Reviewed-by: Varun Khaneja <vakh@chromium.org>
Commit-Queue: Varun Khaneja <vakh@chromium.org>
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();
}
Regression Test / PoC
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 {};
Original Bug Report
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.ccchrome/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.
-
Persistence across Navigation: In
JsOptimizationsPageActionController::PrimaryPageChanged(lines 61-64), the controller callsUpdateIconVisibility()but does not close the activebubble_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. -
Late Origin Binding: The bubble’s OK button is bound to
EnableV8Optimizations()(lines 114-117). When clicked, this function callssite_protection::EnableV8Optimizations(web_contents())(line 143). -
Incorrect Write Target: The helper function
site_protection::EnableV8Optimizations(inchrome/browser/site_protection/site_familiarity_utils.cc:156-176) retrieves the current site URL viaweb_contents()->GetSiteInstance()->GetSecurityPrincipal().GetDeprecatedSiteURL(). If a navigation occurred after the bubble was opened but before the button was clicked, theCONTENT_SETTING_ALLOWforJAVASCRIPT_OPTIMIZERis 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.
- The user navigates to an unfamiliar origin (e.g.,
https://attacker-a.test/). The ‘JavaScript optimizations disabled’ chip appears in the omnibox. - The user clicks the chip to open the bubble.
- While the bubble is open, the attacker page executes a navigation (e.g.,
location.replace('https://attacker-b.test/')) to a different unfamiliar origin. - 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.
Recommended Fix
- Modify
JsOptimizationsPageActionController::PrimaryPageChangedto close the activebubble_if it exists. This ensures the dialog does not persist across navigations. - When creating the bubble in
CreateBubble, the current site URL should be snapshotted and passed to theEnableV8Optimizationscallback, 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.