CVE-2026-79173
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/ui/views/web_apps/frame_toolbar/web_app_origin_text.cc |
modified |
Files Changed
chrome/browser/ui/views/web_apps/frame_toolbar/web_app_origin_text.ccchrome/browser/ui/views/web_apps/frame_toolbar/web_app_origin_text.h
Patch
From 6933105480d7e22ef4b21fd1e1594e69f8bea410 Mon Sep 17 00:00:00 2001 From: Dana Fried <[email protected]> Date: Tue, 21 Jul 2026 14:00:20 -0700 Subject: [PATCH] [Tabbed Web App] Fix spurious incorrect origin text flash This disables the origin text flash on tabbed PWAs when the user switches tabs; it was previously causing a spurious flash of the wrong text any time the user changed tabs and a new page loaded. Explanation in the code. Fixed: 517487890 Change-Id: I6c598f7c7137df68f56af2b5e84020541a7add28 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8124935 Reviewed-by: Dibyajyoti Pal <[email protected]> Commit-Queue: Dana Fried <[email protected]> Cr-Commit-Position: refs/heads/main@{#1665775} --- diff --git a/chrome/browser/ui/views/web_apps/frame_toolbar/web_app_origin_text.cc b/chrome/browser/ui/views/web_apps/frame_toolbar/web_app_origin_text.cc index bcd04650..b8cc178c 100644 --- a/chrome/browser/ui/views/web_apps/frame_toolbar/web_app_origin_text.cc +++ b/chrome/browser/ui/views/web_apps/frame_toolbar/web_app_origin_text.cc @@ -46,7 +46,7 @@ SetLayoutManager(std::make_unique<views::FillLayout>()); label_ = std::make_unique<views::Label>( - origin_text_, ChromeTextContext::CONTEXT_DIALOG_BODY_TEXT_SMALL, + u"", ChromeTextContext::CONTEXT_DIALOG_BODY_TEXT_SMALL, views::style::STYLE_EMPHASIZED) .release(); label_->SetElideBehavior(gfx::ELIDE_HEAD); @@ -129,13 +129,13 @@ void WebAppOriginText::OnLayerAnimationEnded( ui::LayerAnimationSequence* sequence) { SetVisible(false); - last_completed_animation_text_for_testing_ = origin_text_; + last_completed_animation_text_for_testing_ = label_->GetText(); } void WebAppOriginText::OnLayerAnimationAborted( ui::LayerAnimationSequence* sequence) { SetVisible(false); - last_completed_animation_text_for_testing_ = origin_text_; + last_completed_animation_text_for_testing_ = label_->GetText(); } std::u16string_view WebAppOriginText::GetLabelTextForTesting() const { @@ -149,6 +149,15 @@ const TabStripSelectionChange& selection) { if (selection.active_tab_changed() && !tab_strip_model->empty()) { Observe(selection.new_contents); + // Don't do anything if this is the original tab loading in. The text will + // be shown on navigation complete. + const bool is_startup = !selection.old_contents; + if (!is_startup) { + // In tabbed PWAs, the new tab might be from a different origin, in which + // case we don't want to accidentally show the previous tab's or the app's + // origin in the label. Update the text appropriately. + MaybeUpdateAndShowText(selection.new_contents); + } } } @@ -156,26 +165,32 @@ if (!handle->IsInPrimaryMainFrame() || handle->IsSameDocument()) { return; } - content::WebContents* web_contents = handle->GetWebContents(); - if (!web_contents) { + MaybeUpdateAndShowText(handle->GetWebContents()); +} + +void WebAppOriginText::MaybeUpdateAndShowText( + const content::WebContents* new_contents) { + if (!new_contents) { return; } - BrowserWindowInterface* browser = - GlobalBrowserCollection::GetInstance()->FindBrowserWithTab(web_contents); + BrowserWindowInterface* const browser = + GlobalBrowserCollection::GetInstance()->FindBrowserWithTab(new_contents); if (!browser) { return; } - web_app::AppBrowserController* app_controller = + const web_app::AppBrowserController* const app_controller = web_app::AppBrowserController::From(browser); if (!app_controller) { return; } - std::u16string new_origin_text = app_controller->GetLaunchFlashText(); - if (new_origin_text.empty() || new_origin_text == origin_text_) { + const std::u16string new_origin_text = app_controller->GetLaunchFlashText(); + if (new_origin_text.empty() || new_origin_text == label_->GetText()) { return; } - origin_text_ = std::move(new_origin_text); - label_->SetText(origin_text_); + // Note: this will also update the accessible name via the + // `UpdateAccessibleName()` callback. + label_->SetText(new_origin_text); + // CCT UI already displays origin information so there is no need to animate // origin text. // TODO(crbug.com/40282543): Instead of DidFinishNavigation, we can use @@ -187,6 +202,7 @@ label_->layer()->GetAnimator()->StopAnimating(); return; } + StartFadeAnimation(); } diff --git a/chrome/browser/ui/views/web_apps/frame_toolbar/web_app_origin_text.h b/chrome/browser/ui/views/web_apps/frame_toolbar/web_app_origin_text.h index 264bc82..a8edbbc 100644 --- a/chrome/browser/ui/views/web_apps/frame_toolbar/web_app_origin_text.h +++ b/chrome/browser/ui/views/web_apps/frame_toolbar/web_app_origin_text.h @@ -70,10 +70,10 @@ // content::WebContentsObserver: void DidFinishNavigation(content::NavigationHandle* handle) override; - void UpdateAccessibleName(); + // May update the text based on the `new_contents`. + void MaybeUpdateAndShowText(const content::WebContents* new_contents); - // origin_text_ is populated by ReadyToCommitNavigation. - std::u16string origin_text_; + void UpdateAccessibleName(); // Disallow animation until the parent view animates for the first time. This // helps respect the animation start delay in WebAppToolbarButtonContainer.
Original Bug Report
Potential PWA title-bar origin spoofing on tab switch via stale WebAppOriginText
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: A potential security UI spoofing vulnerability in tabbed Progressive Web Apps (PWAs) on ChromeOS allows a stale origin to be displayed over attacker-controlled content on tab switch. When a user switches to a background-loaded tab that has a different theme color, the title-bar ‘WebAppOriginText’ fades in with the previous tab’s origin. This can be used to display a trusted partner’s origin over malicious content.
Affected files:
chrome/browser/ui/views/web_apps/frame_toolbar/web_app_origin_text.ccchrome/browser/ui/web_applications/web_app_browser_controller.ccchrome/browser/ui/web_applications/app_browser_controller.cc
Estimated timestamp from git blame: 2023-10-26
Potential Security UI Origin Spoofing in Tabbed PWAs
There is a potential security UI origin spoofing vulnerability in tabbed PWAs on ChromeOS. When a user switches tabs, WebAppOriginText fails to update its cached origin text, but may still be triggered to fade in due to a theme-color update. This causes the title bar to briefly flash the origin of the previous tab over the newly active tab’s content.
Note: These findings represent a potential vulnerability analyzed through static code inspection. Our tooling does not currently have the capability to execute code or run a working proof of concept.
Root Cause Analysis
In chrome/browser/ui/views/web_apps/frame_toolbar/web_app_origin_text.cc, OnTabStripModelChanged handles tab selection changes but only re-binds the web contents observer:
void WebAppOriginText::OnTabStripModelChanged(
TabStripModel* tab_strip_model,
const TabStripModelChange& change,
const TabStripSelectionChange& selection) {
if (selection.active_tab_changed() && !tab_strip_model->empty()) {
Observe(selection.new_contents); // Only rebinds observer; label_ is untouched
}
}
When a PWA utilizes non-empty scope_extensions, the title bar origin flash text is dynamic per-active-tab. This is because WebAppBrowserController::GetFormattedUrlOrigin() retrieves the formatted URL of the active tab instead of a static app origin.
When switching from Tab A (on partner.example) to an already background-loaded Tab B (on attacker.example), OnTabStripModelChanged only updates the observer. Since Tab B has already finished navigating, DidFinishNavigation is not called, and the label_ text is left holding the stale value of 'partner.example'.
If Tab B has a different <meta name="theme-color"> than Tab A, the tab switch triggers a window theme update through AppBrowserController::UpdateThemePack(). This propagates down the views hierarchy:
BrowserWindowThemeObserver::NotifyThemeChanged -> WebAppFrameToolbarView::OnThemeChanged -> UpdateCaptionColors -> UpdateChildrenColor(true) -> WebAppToolbarButtonContainer::SetColors -> WebAppOriginText::SetTextColor(..., show_text=true).
SetTextColor then invokes StartFadeAnimation(), which fades in the stale 'partner.example' text over the active, attacker-controlled Tab B. Because both origins are within the validated scope (via scope_extensions), no custom tab bar is shown, and the title-bar flash is the only origin indicator displayed to the user.
Potential Replication Steps
- On ChromeOS, install a tabbed PWA from
https://attacker.example/app/whose manifest includes"display_override": ["tabbed"], a valid"scope_extensions"entry forhttps://partner.example, and appropriate origin-association verification. - In Tab A, navigate to
https://partner.example/page(which sets<meta name="theme-color" content="#0000ff">). The title bar flashes'partner.example'and fades out. - Open Tab B in the background on
https://attacker.example/app/phish(which sets<meta name="theme-color" content="#ff0000">) and wait for it to finish loading. - Click Tab B in the tab strip.
- Observe that the title bar incorrectly fades in and displays
'partner.example'for several seconds while Tab B’s attacker-controlled content is active.
Proposed Fix
In WebAppOriginText::OnTabStripModelChanged, the origin text cache and label should be explicitly updated to reflect the new tab’s origin when the active tab changes, similar to how CustomTabBarView handles this event. For example, retrieve the AppBrowserController associated with the new web contents (or store a pointer to Browser during construction) and refresh the label text:
void WebAppOriginText::OnTabStripModelChanged(
TabStripModel* tab_strip_model,
const TabStripModelChange& change,
const TabStripSelectionChange& selection) {
if (selection.active_tab_changed() && !tab_strip_model->empty()) {
Observe(selection.new_contents);
// Retrieve the controller and update the label immediately
BrowserWindowInterface* browser =
GlobalBrowserCollection::GetInstance()->FindBrowserWithTab(selection.new_contents);
if (browser) {
if (auto* app_controller = web_app::AppBrowserController::From(browser)) {
origin_text_ = app_controller->GetLaunchFlashText();
label_->SetText(origin_text_);
}
}
}
}
Evaluated with Chrome root at commit: b1520ef4a76878853a31f0943b565e42060edec8
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.