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

Overview

Medium
Severity
5.4
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactIncorrect security UI in Digital Credentials
DescriptionIncorrect security UI in Digital Credentials
ComponentDigital Credentials
Sub-componentDC
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.
Tracker452209495
Fix commit9f349d7e49bf (chromium/src) +32/-6
CWECWE-451
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:L/A:N
CISA KEVNot listed
CreditedHafiizh
Reported2025-10-15 · bounty $1000 (per vendor advisory)
Fix landed2025-11-03 (public git)
Disclosed2026-01-13
Patch gap71 days — fix public in source before the advisory

Background

Digital Credentials API
a web API that lets a relying-party (RP) site request or issue digital identity credentials. On desktop, Chrome shows browser-owned dialogs, such as a QR-code dialog and a safety interstitial, that name the requesting origin rp_origin_.
`url_formatter::FormatOriginForSecurityDisplay`
formats an origin as a full string for security UI with no width awareness, so an arbitrarily long host is passed through intact.
`url_formatter::ElideUrl`
elides a URL to fit a given pixel width for a given gfx::FontList. It shortens the less significant parts so the identifying part of the host stays visible.
Security UI spoofing
making browser-drawn UI show the user a misleading identity for the requesting site, so they trust a request they would otherwise reject.

Root Cause Analysis

Two desktop Digital Credentials UIs put the RP origin into a localised body string with FormatOriginForSecurityDisplay(rp_origin_, OMIT_CRYPTOGRAPHIC). Neither formatting call limited the result to the space the dialog actually has. One is the dialog built in digital_identity_provider_desktop.cc, which uses body IDs such as IDS_WEB_DIGITAL_CREDENTIALS_ISSUANCE_QR_BODY. The other is the safety interstitial in digital_identity_safety_interstitial_controller_desktop.cc. In neither hunk is the enclosing function’s name shown. An attacker who controls a very long origin, such as a long subdomain chain that starts with a trusted-looking name, could have the meaningful part of the host pushed out of view or wrapped confusingly by the dialog label. The user would then see a misleading identity for the requester (inferred: the exact rendering is in the referenced bug, not the diff).

The fix computes the available content width from views::LayoutProvider (DISTANCE_MODAL_DIALOG_PREFERRED_WIDTH minus INSETS_DIALOG) and the label’s font from views::TypographyProvider. It then formats the origin with url_formatter::ElideUrl(rp_origin_.GetURL(), font_list, content_width), so elision keeps the security-relevant part of the origin visible within the dialog width. This tightens how the origin is displayed. No access-control check changes.

Key insight
The security dialogs showed an unbounded, width-unaware origin string, so the dialog layout, not the browser’s origin-eliding logic, decided which part of a long attacker-chosen host the user saw. The fix elides the origin to the dialog’s content width with ElideUrl, so the distinguishing part of the host stays visible.

Attack Path

  1. Host a site on a long origin The attacker serves a page from an origin with a very long host whose leading labels imitate a trusted party (inferred: the diff does not show the specific origin).
  2. Invoke the Digital Credentials API The page makes a digital credential request or issuance call, and Chrome desktop shows the QR-code dialog or the safety interstitial that names rp_origin_.
  3. Origin rendered misleadingly Before the fix, FormatOriginForSecurityDisplay produced the full host and the dialog did not elide it to its width, so the identifying part of the host could be hidden or ambiguous.
  4. User trusts the spoofed requester The user proceeds, for example by scanning the QR code with a wallet or accepting the interstitial, believing the request comes from the trusted party (inferred), which could disclose identity credentials to the attacker (inferred).

Impact Assessment

A malicious website can make browser-owned Digital Credentials dialogs on desktop show a misleading requester origin. This is a UI-spoofing issue in the browser process, not a memory-safety bug. The preconditions are a desktop build where the Digital Credentials API flow and these dialogs are available (the diff does not show the feature gating), an attacker-chosen long origin, and user interaction to complete the credential flow. Any resulting credential disclosure is (inferred). The vendor rates it medium severity (“Incorrect security UI in Digital Credentials”). The advisory text given here says nothing about in-the-wild exploitation.

Files Changed

  • chrome/browser/digital_credentials/digital_identity_provider_desktop.cc
  • chrome/browser/ui/views/digital_credentials/digital_identity_safety_interstitial_controller_desktop.cc

Audit Directions

  • Origins interpolated into dialog strings
    Search for FormatOriginForSecurityDisplay or FormatUrlForSecurityDisplay results passed into l10n_util::GetStringFUTF16 for permission, payment, WebAuthn, FedCM and file-access dialogs where no width-aware elision is applied.
  • Elision width versus real layout
    Where ElideUrl is used, check that the font context and width match the label that renders the text (multi-line bodies, Android/iOS equivalents, RTL), since a mismatch can bring back truncation by the label.
  • Parallel platform UIs
    The fix covers only _desktop files. Review the Android and other platform implementations of the same Digital Credentials prompts for the same unbounded origin display.
From 9f349d7e49bf8f162840ce82f3368fc81996ff16 Mon Sep 17 00:00:00 2001
From: Mohamed Amir Yosef <mamir@chromium.org>
Date: Mon, 03 Nov 2025 11:08:40 -0800
Subject: [PATCH] [DC] Elide the origin for display in the interstitial

This CL properly elides the origins to be displayed in the digital
credentials UIs.

More details are in the linked bug.

Bug: 452209495

Change-Id: Icdf97292f42b91c7911a0397c7f40ab0659f1b82
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7083387
Reviewed-by: Elly FJ <ellyjones@chromium.org>
Commit-Queue: Mohamed Amir Yosef <mamir@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1539496}
---

diff --git a/chrome/browser/digital_credentials/digital_identity_provider_desktop.cc b/chrome/browser/digital_credentials/digital_identity_provider_desktop.cc
index 9e426ed4..d84f860b 100644
--- a/chrome/browser/digital_credentials/digital_identity_provider_desktop.cc
+++ b/chrome/browser/digital_credentials/digital_identity_provider_desktop.cc
@@ -36,6 +36,9 @@
 #include "ui/views/controls/image_view.h"
 #include "ui/views/controls/theme_tracking_animated_image_view.h"
 #include "ui/views/layout/box_layout_view.h"
+#include "ui/views/layout/layout_provider.h"
+#include "ui/views/style/typography.h"
+#include "ui/views/style/typography_provider.h"
 #include "ui/views/widget/widget.h"
 
 namespace {
@@ -284,10 +287,21 @@
       dialog_body_id = IDS_WEB_DIGITAL_CREDENTIALS_ISSUANCE_QR_BODY;
       break;
   }
-  std::u16string dialog_body = l10n_util::GetStringFUTF16(
-      dialog_body_id,
-      url_formatter::FormatOriginForSecurityDisplay(
-          rp_origin_, url_formatter::SchemeDisplay::OMIT_CRYPTOGRAPHIC));
+  const views::LayoutProvider* layout_provider = views::LayoutProvider::Get();
+  // The dialog content width is used as an approximation of the available
+  // width for the origin.
+  const int dialog_width = layout_provider->GetDistanceMetric(
+      views::DISTANCE_MODAL_DIALOG_PREFERRED_WIDTH);
+  const gfx::Insets dialog_insets =
+      layout_provider->GetInsetsMetric(views::INSETS_DIALOG);
+  const float content_width = dialog_width - dialog_insets.width();
+  const gfx::FontList& font_list = views::TypographyProvider::Get().GetFont(
+      views::style::CONTEXT_LABEL, views::style::STYLE_PRIMARY);
+  std::u16string formatted_origin =
+      url_formatter::ElideUrl(rp_origin_.GetURL(), font_list, content_width);
+
+  std::u16string dialog_body =
+      l10n_util::GetStringFUTF16(dialog_body_id, formatted_origin);
   EnsureDialogCreated()->TryShow(
       /*accept_button=*/std::nullopt, base::OnceClosure(),
       /*cancel_button=*/
diff --git a/chrome/browser/ui/views/digital_credentials/digital_identity_safety_interstitial_controller_desktop.cc b/chrome/browser/ui/views/digital_credentials/digital_identity_safety_interstitial_controller_desktop.cc
index 5c56236..27298d4a 100644
--- a/chrome/browser/ui/views/digital_credentials/digital_identity_safety_interstitial_controller_desktop.cc
+++ b/chrome/browser/ui/views/digital_credentials/digital_identity_safety_interstitial_controller_desktop.cc
@@ -18,6 +18,10 @@
 #include "ui/base/l10n/l10n_util.h"
 #include "ui/base/models/dialog_model.h"
 #include "ui/base/mojom/dialog_button.mojom.h"
+#include "ui/display/screen.h"
+#include "ui/views/layout/layout_provider.h"
+#include "ui/views/style/typography.h"
+#include "ui/views/style/typography_provider.h"
 #include "ui/views/widget/widget.h"
 
 using DialogButton = ui::DialogModel::Button;
@@ -106,9 +110,17 @@
 
   bool positive_button_enabled = !was_request_aborted;
 
+  const views::LayoutProvider* layout_provider = views::LayoutProvider::Get();
+  const int dialog_width = layout_provider->GetDistanceMetric(
+      views::DISTANCE_MODAL_DIALOG_PREFERRED_WIDTH);
+  const gfx::Insets dialog_insets =
+      layout_provider->GetInsetsMetric(views::INSETS_DIALOG);
+  const float content_width = dialog_width - dialog_insets.width();
+
+  const gfx::FontList& font_list = views::TypographyProvider::Get().GetFont(
+      views::style::CONTEXT_DIALOG_BODY_TEXT, views::style::STYLE_PRIMARY);
   std::u16string formatted_origin =
-      url_formatter::FormatOriginForSecurityDisplay(
-          rp_origin_, url_formatter::SchemeDisplay::OMIT_CRYPTOGRAPHIC);
+      url_formatter::ElideUrl(rp_origin_.GetURL(), font_list, content_width);
   std::u16string body_text =
       l10n_util::GetStringFUTF16(body_resource_id, formatted_origin);
   std::u16string positive_button_label = l10n_util::GetStringUTF16(
Loading diff…

Original Bug Report

reported by sa...@gmail.com

truncated long domain on Digital Credentials API prompt lead to spoof

VULNERABILITY DETAILS

When the Digital Credentials API dialog is invoked, the dialog appears with the name “Do you trust this site with your data” and the long domain name causes the domain name to be truncated. As seen in the second video at https://developer.chrome.com/blog/digital-credentials-api-shipped?hl=en . In the second video, the dialog appears to be “9000–firebase-studio-1758636784187.cluster-64pjnskmlbaxowh5lzq6i7v4ra.clo” instead of 9000–firebase-studio-1758636784187.cluster-64pjnskmlbaxowh5lzq6i7v4ra.cloudworkstations.dev, thus causing the dialog to be spoofed.

  1. Create a Digital Credentials API application using the example at https://developer.chrome.com/blog/digital-credentials-api-shipped?hl=en using a long domain.
  2. Open the page as shown on https://verifier.multipaz.org/ and click one of the button and show the confirm dialog
View on issue tracker