Medium watch CVSS 5.4 chrome Spoofing 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
5.4
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUI misrepresentation in WebAppInstalls
DescriptionUI misrepresentation in WebAppInstalls
ComponentWebAppInstalls
Sub-componentTabbed Web App
Bug ClassSpoofing
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker517487890
Fix commit6933105480d7 (chromium/src) +31/-15
CWECWE-451
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:N/A:L
CISA KEVNot listed
CreditedGoogle
Reported2026-05-28 (per vendor advisory)
Fix landed2026-07-21 (public git)
Disclosed2026-08-25
Patch gap35 days — fix public in source before the advisory

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_contents and new_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.

Key insight
The single mistake was caching the origin in 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

  1. 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).
  2. Prime the origin label One tab displays a trusted origin, so WebAppOriginText caches that origin in origin_text_ / the label.
  3. Switch to an attacker tab The user switches to a tab whose content is a different (attacker) origin; pre-patch OnTabStripModelChanged re-observes the tab but leaves the previously shown origin text in place.
  4. 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

An attacker gains a UI-misrepresentation/spoofing opportunity: the PWA frame’s origin flash can briefly show an origin that does not match the content currently displayed, potentially misleading the user about which site they are on, entirely within the browser UI. The precondition is a tabbed PWA with tab switches between differing origins; the advisory classifies this as UI misrepresentation in WebAppInstalls and reports no in-the-wild exploitation. No memory-safety or privilege consequence is implied; impact is limited to a transient misleading origin display.

Changed Functions

FunctionChangeNotes
if
chrome/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.h
  • chrome/browser/ui/views/web_apps/frame_toolbar/web_app_origin_text.cc

Audit Directions

  • Cached security-indicator state
    Audit 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 coverage
    In 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 values
    Check that indicator views start empty rather than pre-seeded with a value that may not match the first content shown.
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.
Loading diff…

Original Bug Report

reported by vm...@google.com

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.cc
  • chrome/browser/ui/web_applications/web_app_browser_controller.cc
  • chrome/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

  1. On ChromeOS, install a tabbed PWA from https://attacker.example/app/ whose manifest includes "display_override": ["tabbed"], a valid "scope_extensions" entry for https://partner.example, and appropriate origin-association verification.
  2. 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.
  3. 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.
  4. Click Tab B in the tab strip.
  5. 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.

View on issue tracker