CVE-2026-79173
Overview
Background
- Tabbed web app (PWA)
- an installed Progressive Web App window that can host multiple tabs, potentially from different origins within the app.
- `WebAppOriginText`
- the toolbar view that briefly flashes the current page’s origin (“launch flash text”) in a PWA window frame to inform the user which origin they are viewing.
- `origin_text_`
- the removed member that cached the last origin string; the label’s own text (
label_->GetText()) is now the single source of truth for what is displayed. - `TabStripSelectionChange` / `OnTabStripModelChanged`
- the notification delivered when the active tab changes, carrying
old_contentsandnew_contents.
Root Cause Analysis
WebAppOriginText cached the displayed origin in origin_text_ and only refreshed it in DidFinishNavigation, initializing the label with origin_text_ and, on animation end/abort, recording origin_text_ as the completed text. When the user switched tabs in a tabbed PWA, the active-tab-changed path (OnTabStripModelChanged) re-observed the new WebContents but did not update the origin text, so the label still held the previous tab’s or the app’s origin; a subsequent navigation in the new tab would then flash origin text that, transiently, did not match the content actually being shown — the “spurious flash of the wrong text.” The invariant violated is that the flashed origin must correspond to the tab/content currently active.
The fix factors the update logic into MaybeUpdateAndShowText(new_contents) and calls it from the active-tab-changed branch (except on startup, when old_contents is null and the text will be shown on navigation complete), so the label is refreshed to the new tab’s origin — or cleared/left correct — immediately on tab switch. It also removes origin_text_ and drives comparisons and the completed-animation record off label_->GetText(), eliminating the stale cached value, and starts the label with empty text (u"") rather than a possibly wrong initial origin.
origin_text_ and refreshing it only on navigation, so a tab switch left the stale origin displayed; the fix updates the shown origin on active-tab change via MaybeUpdateAndShowText and makes the label’s own text the source of truth.Attack Path
- Install a multi-origin tabbed PWA The user has a tabbed PWA whose tabs can host content from more than one origin (including attacker-controlled content).
- Prime the origin label
One tab displays a trusted origin, so
WebAppOriginTextcaches that origin inorigin_text_/ the label. - Switch to an attacker tab
The user switches to a tab whose content is a different (attacker) origin; pre-patch
OnTabStripModelChangedre-observes the tab but leaves the previously shown origin text in place. - Flash the wrong origin When a navigation then completes in the active tab, the origin flash briefly shows text mismatched to the actual content, misrepresenting which origin the user is viewing (UI spoofing).
Impact Assessment
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.hchrome/browser/ui/views/web_apps/frame_toolbar/web_app_origin_text.cc
Audit Directions
- Cached security-indicator stateAudit UI that caches a security-relevant string (origin, lock state, permission) separately from the widget and only refreshes on some events; verify every state transition (tab switch, focus change) updates the indicator.
- Tab-switch vs navigation coverageIn multi-tab surfaces (tabbed PWAs, CCT), confirm that active-tab-changed handlers, not just navigation handlers, refresh per-tab UI to the newly active
WebContents. - Initial/empty-state valuesCheck that indicator views start empty rather than pre-seeded with a value that may not match the first content shown.
Patch
From 6933105480d7e22ef4b21fd1e1594e69f8bea410 Mon Sep 17 00:00:00 2001
From: Dana Fried <dfried@chromium.org>
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 <dibyapal@chromium.org>
Commit-Queue: Dana Fried <dfried@chromium.org>
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.