CVE-2026-0904
Overview
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.
ElideUrl, so the distinguishing part of the host stays visible.Attack Path
- 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).
- 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_. - Origin rendered misleadingly
Before the fix,
FormatOriginForSecurityDisplayproduced 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. - 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
Files Changed
chrome/browser/digital_credentials/digital_identity_provider_desktop.ccchrome/browser/ui/views/digital_credentials/digital_identity_safety_interstitial_controller_desktop.cc
Audit Directions
- Origins interpolated into dialog stringsSearch for
FormatOriginForSecurityDisplayorFormatUrlForSecurityDisplayresults passed intol10n_util::GetStringFUTF16for permission, payment, WebAuthn, FedCM and file-access dialogs where no width-aware elision is applied. - Elision width versus real layoutWhere
ElideUrlis 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 UIsThe fix covers only
_desktopfiles. Review the Android and other platform implementations of the same Digital Credentials prompts for the same unbounded origin display.
Patch
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(
Original Bug Report
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.
- 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.
- Open the page as shown on https://verifier.multipaz.org/ and click one of the button and show the confirm dialog