Overview

Low
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in Permissions
DescriptionInappropriate implementation in Permissions
ComponentPermissions
Bug ClassLogic Error
Tracker503614310
Fix commit60cd051f1e9d (chromium/src) +5/-10
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Files Changed

  • chrome/browser/ui/content_settings/content_setting_bubble_model.cc
From 60cd051f1e9d7dd06ac26ef850489992958fce9c Mon Sep 17 00:00:00 2001
From: Antonio Sartori <antoniosartori@chromium.org>
Date: Mon, 20 Apr 2026 08:47:12 -0700
Subject: [PATCH] [permissions] Fix URL used in ContentSettingsMediaStreamBubbleView

This CL ensures that the URL used in
ContentSettingsMediaStreamBubbleView when updating content settings is
the same as the URL displayed to the user in the bubble.

Bug: 503614310
Change-Id: Ie617e0a2e3b231a43c685ede1f165991c3f77dca
Fixed: 503614310
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7772434
Commit-Queue: Antonio Sartori <antoniosartori@chromium.org>
Reviewed-by: Judith Hemp <hempjudith@google.com>
Cr-Commit-Position: refs/heads/main@{#1617514}
---

diff --git a/chrome/browser/ui/content_settings/content_setting_bubble_model.cc b/chrome/browser/ui/content_settings/content_setting_bubble_model.cc
index 9f881791..85cd62dd 100644
--- a/chrome/browser/ui/content_settings/content_setting_bubble_model.cc
+++ b/chrome/browser/ui/content_settings/content_setting_bubble_model.cc
@@ -1276,8 +1276,7 @@
 
 void ContentSettingMediaStreamBubbleModel::UpdateSettings(
     ContentSetting setting) {
-  PageSpecificContentSettings* page_content_settings =
-      PageSpecificContentSettings::GetForFrame(&GetPage().GetMainDocument());
+  const GURL& url = bubble_content().radio_group.url;
   // The same urls must be used as in other places (e.g. the infobar) in
   // order to override the existing rule. Otherwise a new rule is created.
   // TODO(markusheintz): Extract to a helper so that there is only a single
@@ -1287,24 +1286,20 @@
   if (MicrophoneAccessed()) {
     permissions::PermissionUmaUtil::ScopedRevocationReporter
         scoped_revocation_reporter(
-            GetProfile(), page_content_settings->media_stream_access_origin(),
-            GURL(), ContentSettingsType::MEDIASTREAM_MIC,
+            GetProfile(), url, GURL(), ContentSettingsType::MEDIASTREAM_MIC,
             permissions::PermissionSourceUI::PAGE_ACTION);
     map->SetContentSettingDefaultScope(
-        page_content_settings->media_stream_access_origin(), GURL(),
-        ContentSettingsType::MEDIASTREAM_MIC, setting,
+        url, GURL(), ContentSettingsType::MEDIASTREAM_MIC, setting,
         CreateConstraintsForAutoRevocation(ContentSettingsType::MEDIASTREAM_MIC,
                                            setting));
   }
   if (CameraAccessed()) {
     permissions::PermissionUmaUtil::ScopedRevocationReporter
         scoped_revocation_reporter(
-            GetProfile(), page_content_settings->media_stream_access_origin(),
-            GURL(), ContentSettingsType::MEDIASTREAM_CAMERA,
+            GetProfile(), url, GURL(), ContentSettingsType::MEDIASTREAM_CAMERA,
             permissions::PermissionSourceUI::PAGE_ACTION);
     map->SetContentSettingDefaultScope(
-        page_content_settings->media_stream_access_origin(), GURL(),
-        ContentSettingsType::MEDIASTREAM_CAMERA, setting,
+        url, GURL(), ContentSettingsType::MEDIASTREAM_CAMERA, setting,
         CreateConstraintsForAutoRevocation(
             ContentSettingsType::MEDIASTREAM_CAMERA, setting));
   }
Loading diff…

Original Bug Report

reported by rj...@google.com

Potential TOCTOU permission spoofing in Media Stream Bubble via navigation

Flapjack, 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 without the Chrome Security team. Please see go/chrome-ai-generated-security-bugs-faq for more information.

Overview: A potential Time-of-Check to Time-of-Use (TOCTOU) vulnerability exists in the Media Stream permission bubble. If an attacker navigates the tab while the user is interacting with the bubble, the user’s permission choice may be saved to the newly navigated origin instead of the original requester.

Affected files:

  • chrome/browser/ui/content_settings/content_setting_bubble_model.cc

Estimated timestamp from git blame: 2023-07-19

Summary

A potential Time-of-Check to Time-of-Use (TOCTOU) vulnerability exists in ContentSettingMediaStreamBubbleModel. When the user interacts with the camera/microphone permission bubble (e.g., clicking the omnibox icon to change a blocked setting to allowed), the model saves this choice when the bubble closes.

However, ContentSettingMediaStreamBubbleModel::CommitChanges() and UpdateSettings() dynamically retrieve the target origin from the currently active document (GetPage().GetMainDocument()) at the time of closure, rather than using the origin that was active when the bubble was opened. Because widget teardown is asynchronous (especially on macOS or via posted tasks), an attacker can race the teardown process by navigating the page to a malicious origin. If the malicious origin requests media access during this tight window, the user’s “Allow” choice is misattributed and persistently saved for the attacker’s origin.

Technical Details

Chromium uses a commit-on-close model for omnibox permission bubbles. When a navigation occurs, ContentSettingBubbleContents::PrimaryPageChanged fires and calls GetWidget()->Close(). This closure is asynchronous; WindowClosing() and CommitChanges() are deferred.

During this deferral, the renderer begins executing JavaScript for the newly committed document. If the new document calls navigator.mediaDevices.getUserMedia(), the browser processes this IPC.

Crucially, CommitChanges() has a safety check: it aborts if content_settings->media_stream_access_origin().is_empty(). If the attacker’s origin is in the default ASK state, getUserMedia() triggers a prompt, and the origin remains empty, neutralizing the exploit.

However, if the attacker’s origin is previously set to BLOCK, the permission request is denied synchronously (MediaStreamDevicesController::RequestPermissions calls its callback immediately). This synchronous callback reaches PermissionBubbleMediaAccessHandler::OnMediaStreamRequestResponse, which populates media_stream_access_origin_ with the attacker’s origin before the asynchronous widget teardown completes. When CommitChanges() finally executes, it reads the attacker’s origin and applies the user’s pending “Allow” choice to it.

Potential Attack Steps

Note: These are suggested/potential steps derived from code analysis; our tooling cannot execute a live proof-of-concept to confirm the exact race timings.

  1. The attacker convinces the user to block camera access on https://evil.com (saving a BLOCK setting).
  2. The attacker opens https://victim.com in a popup, which also has its camera access blocked.
  3. The user clicks the blocked camera icon in the victim.com omnibox, opening the Media Stream bubble.
  4. The user clicks the radio button to “Always allow https://victim.com to access your camera”.
  5. Before the user clicks “Done” or clicks away, the attacker’s script navigates the popup to https://evil.com.
  6. The navigation initiates an asynchronous close of the bubble.
  7. https://evil.com immediately calls navigator.mediaDevices.getUserMedia({video: true}).
  8. Because evil.com is already blocked, the request resolves synchronously, overwriting the page’s media_stream_access_origin_ with https://evil.com.
  9. The asynchronous bubble closure completes, triggering CommitChanges().
  10. CommitChanges() fetches the current origin (https://evil.com) and saves the user’s “Allow” selection to the attacker’s origin.

Suggested Fix

To prevent this and similar race conditions, the target origin (and potentially the relevant PageSpecificContentSettings state) should be cached in the constructor of ContentSettingMediaStreamBubbleModel when the bubble is created.

In CommitChanges() and UpdateSettings(), the model should use this immutable cached origin instead of dynamically querying PageSpecificContentSettings::GetForFrame(&GetPage().GetMainDocument()). This pattern matches the fix applied previously to ContentSettingStorageAccessBubbleModel.

Evaluated with Chrome root at commit: c0eb5541aebfa4ea08806eaf6e94bcc69f87ab2f


Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; 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