CVE-2026-11300
Overview
Files Changed
chrome/browser/ui/content_settings/content_setting_bubble_model.cc
Patch
From 60cd051f1e9d7dd06ac26ef850489992958fce9c Mon Sep 17 00:00:00 2001 From: Antonio Sartori <[email protected]> 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 <[email protected]> Reviewed-by: Judith Hemp <[email protected]> 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)); }
Original Bug Report
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.
- The attacker convinces the user to block camera access on
https://evil.com(saving aBLOCKsetting). - The attacker opens
https://victim.comin a popup, which also has its camera access blocked. - The user clicks the blocked camera icon in the
victim.comomnibox, opening the Media Stream bubble. - The user clicks the radio button to “Always allow https://victim.com to access your camera”.
- Before the user clicks “Done” or clicks away, the attacker’s script navigates the popup to
https://evil.com. - The navigation initiates an asynchronous close of the bubble.
https://evil.comimmediately callsnavigator.mediaDevices.getUserMedia({video: true}).- Because
evil.comis already blocked, the request resolves synchronously, overwriting the page’smedia_stream_access_origin_withhttps://evil.com. - The asynchronous bubble closure completes, triggering
CommitChanges(). 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.