CVE-2026-112994
Overview
Files Changed
chrome/browser/ui/views/content_setting_site_row_view.cc
Patch
From cbdaf1b9bfdcb018b00555aaccf9df16888acbf1 Mon Sep 17 00:00:00 2001
From: Dana Fried <dfried@chromium.org>
Date: Thu, 17 Sep 2026 14:10:15 -0700
Subject: [PATCH] [Content Settings] Use ELIDE_HEAD to avoid risk of URL spoofing
Single-line URL fields should preferably use ELIDE_HEAD so that the
last elements of the host are visible, not the first.
Compare:
mail.google.unsafe.com -> mail.google....
vs.
mail.google.unsafe.com -> ...e.unsafe.com
This CL will produce the latter.
Fixed: 562962437
Change-Id: I01c027079ebc5d4a6f4c8294b0a6bb9e4f6f6a2a
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8427182
Auto-Submit: Dana Fried <dfried@chromium.org>
Reviewed-by: Foromo Daniel Soromou <koretadaniel@chromium.org>
Commit-Queue: Dana Fried <dfried@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1700114}
---
diff --git a/chrome/browser/ui/views/content_setting_site_row_view.cc b/chrome/browser/ui/views/content_setting_site_row_view.cc
index 9c989fa9..0364037 100644
--- a/chrome/browser/ui/views/content_setting_site_row_view.cc
+++ b/chrome/browser/ui/views/content_setting_site_row_view.cc
@@ -17,6 +17,7 @@
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/base/models/image_model.h"
#include "ui/base/ui_base_features.h"
+#include "ui/gfx/text_constants.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/controls/button/toggle_button.h"
#include "ui/views/controls/image_view.h"
@@ -60,6 +61,9 @@
auto* title_label = AddChildView(std::make_unique<views::Label>(title));
title_label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
+ // Elide from the head to ensure the registrable-domain suffix stays visible
+ // next to the permission toggle (see docs/security/url_display_guidelines).
+ title_label->SetElideBehavior(gfx::ELIDE_HEAD);
title_label->SetProperty(
views::kFlexBehaviorKey,
views::FlexSpecification(views::LayoutOrientation::kHorizontal,
Original Bug Report
Origin spoof in Storage Access bubble due to tail elision of long domains
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: The Storage Access omnibox bubble renders each embedded site’s identity in ContentSettingSiteRowView with the views::Label default tail elision, inside a bubble hard-fixed at 320px. A registrable domain longer than ~35 characters is displayed with its end deleted — the registry suffix (.com, .github.io, …), the part Chromium’s URL display guidelines require to stay visible — while the adjacent toggle is a live control that persists a 30-day cross-site cookie-access grant for that site. A web attacker who registers a long lookalike eTLD+1 gets its googley head rendered with an ellipsis in place of the disambiguating suffix, on the exact surface where the user decides whether to grant it storage access. Demonstrated end-to-end with an executed web-content POC (attached).
Affected files:
chrome/browser/ui/views/content_setting_site_row_view.cc:61-68— site-identity label created withoutSetElideBehavior(), defaults togfx::ELIDE_TAILchrome/browser/ui/views/content_setting_bubble_contents.cc:382-383— bubble fixed atDISTANCE_BUBBLE_PREFERRED_WIDTH(320px), so the suffix is unrecoverable at any widthchrome/browser/ui/content_settings/content_setting_bubble_model.cc:850-883— toggle state is committed as a persistentSTORAGE_ACCESSgrant on bubble close
Estimated timestamp from git blame: 2023-06-12
Summary
The row label is the identity a user reads before flipping the adjacent cross-site-cookie toggle. It is populated from a net::SchemefulSite (eTLD+1) with no length cap, and the label’s default gfx::ELIDE_TAIL (ui/views/controls/label.h:542) deletes the string’s tail when it overflows the ~220px available inside the fixed 320px bubble (~33–40 characters).
For a long eTLD+1, the deleted tail is the registry suffix — e.g. the 65-char registrable domain accounts-google-com-secure-signin-verification-gateway-portal.com renders as accounts-google-com-secure..., and a 39-char GitHub Pages tenant renders with .github.io hidden. This inverts docs/security/url_display_guidelines/url_display_guidelines.md:109-111: elide hostnames from the front and “Ensure that at least the registrable domain can be shown”. Sibling trust surfaces set gfx::ELIDE_HEAD for exactly this reason (chrome/browser/ui/views/web_apps/web_app_views_utils.cc:51-52 — “Elide from head to prevent origin spoofing”).
The spoof is bounded: net::SchemefulSite collapses hosts to eTLD+1 (net/base/schemeful_site.cc:109-133), so the visible head is a single hyphenated DNS label of the attacker’s own registrable domain — dotted spoofs like accounts.google.com.… are structurally impossible, and the persisted grant is scoped to the same eTLD+1 the row displays. The bug hides the disambiguating suffix of a lookalike domain; it cannot render another site’s dotted identity.
Existing partial mitigations do not fix the rendered decision surface: the toggle’s accessibility name carries the full string (content_setting_site_row_view.cc:74) and the label auto-tooltips on hover, but neither is visible in the row as rendered.
Root Cause Analysis
- Identity formatted with no length management:
chrome/browser/ui/views/content_setting_site_row_view.cc:58-59— the site (already collapsed to eTLD+1) is formatted viaFormatUrlForSecurityDisplay, which applies no length cap (components/url_formatter/elide_url.cc:351-393). - Label defaults to tail elision:
content_setting_site_row_view.cc:61-68— noSetElideBehavior()call, so the defaultgfx::ELIDE_TAILapplies:// [`chrome/browser/ui/views/content_setting_site_row_view.cc:61-63`](https://source.chromium.org/chromium/chromium/src/+/main:chrome/browser/ui/views/content_setting_site_row_view.cc;l=61) auto* title_label = AddChildView(std::make_unique<views::Label>(title)); title_label->SetHorizontalAlignment(gfx::ALIGN_LEFT); // <-- no SetElideBehavior(gfx::ELIDE_HEAD); default ELIDE_TAIL (label.h:542) - Fixed-width bubble makes the suffix unrecoverable:
chrome/browser/ui/views/content_setting_bubble_contents.cc:382-383—set_fixed_width(DISTANCE_BUBBLE_PREFERRED_WIDTH)(320px;ui/views/layout/layout_provider.h:260). After row insets, favicon + margin, and the toggle, the label gets ~220px, so any eTLD+1 beyond ~35 chars loses its tail at every DPI/font plausible for this dialog. - The adjacent toggle is a persistent permission control:
content_setting_site_row_view.cc:80-82→ContentSettingStorageAccessBubbleModel::OnSiteRowClicked→CommitChanges(chrome/browser/ui/content_settings/content_setting_bubble_model.cc:850-883) writesCONTENT_SETTING_ALLOWforContentSettingsType::STORAGE_ACCESSwithkStorageAccessAPIExplicitPermissionLifetime(30 days) — durable unpartitioned cookie access for the misrendered site under the current top-level site.
Reproduction
Demonstrated by the attached executed POC (scripted, deterministic across runs, driven by real input against a Linux desktop build; profile in the standard “Block third-party cookies” mode the Storage Access UI is designed for):
- Attacker controls a long registrable domain, e.g.
accounts-google-com-secure-signin-verification-gateway-portal.com(61-char label +.com), and gets one top-level click on it (satisfies the Storage Access API’s top-level-interaction heuristic). - A page on an unrelated top-level site embeds an attacker iframe; after a click inside the iframe, the frame calls
document.requestStorageAccess(), showing the real permission prompt. Any outcome (deny/dismiss/grant) makes the Storage Access omnibox icon appear. - Victim clicks the omnibox icon: the bubble lists the attacker site as
accounts-google-com-secure...next to the toggle. - Victim flips the toggle and the bubble closes.
- Expected: the row shows at least the registrable-domain tail (per the URL display guidelines), e.g.
...verification-gateway-portal.com.
Observed (executed): the row renders accounts-google-com-secure... — the registry suffix .com is never displayed at any point in the flow — and after toggle + close the profile persists setting: 1 (ALLOW) for pattern https://[*.]accounts-google-com-secure-signin-verification-gateway-portal.com,https://[*.]my-favorite-news-site.com with lifetime = 2,592,000,000,000 µs (30 days). A one-line SetElideBehavior(gfx::ELIDE_HEAD) build was also verified to render ...ification-gateway-portal.com instead, confirming the root cause.
Severity
low — Incorrect security UI (CWE-451) on a permission decision surface: a web attacker needs no compromised renderer, but the deception is bounded (the visible head is a single undotted label of the attacker’s own eTLD+1, so this strengthens a lookalike-domain ruse rather than impersonating another site’s dotted identity), the resulting grant is limited in scope (the attacker site’s own unpartitioned cookies under one top-level site, 30-day lifetime), and exploitation requires several ordinary user interactions (iframe click, opening the chip, flipping the toggle). No memory corruption. This matches the established tier for the “Incorrect security UI in Permissions / domain spoofing” class (e.g. CVE-2026-5905, Chromium severity Low).
Suggested Fix Direction
Set title_label->SetElideBehavior(gfx::ELIDE_HEAD); in ContentSettingSiteRowView, matching web_app_views_utils.cc:51-52, so the registrable-domain tail stays visible (verified effective). The same audit should cover the sibling Page Info two-site permission rows fed from the same GetTwoSitePermissionRequesters data (components/page_info/page_info.cc:1570,1612-1621) and the Storage Access prompt title already tracked by TODO(crbug.com/40064079) (permission_prompt_bubble_two_origins_view.cc:129-157).
POC: Storage Access bubble site row tail-elides long registrable domains next to live permission toggles (missing ELIDE_HEAD)
Strategy: web-content Bug Type: Incorrect security UI / identity misrepresentation (CWE-451) — no memory error; the oracle is the rendered UI plus the persisted content setting.
ContentSettingSiteRowView (chrome/browser/ui/views/content_setting_site_row_view.cc)
renders the embedded site’s identity with the views::Label default
gfx::ELIDE_TAIL and no length management. The bubble is hard-fixed at 320px
(set_fixed_width(DISTANCE_BUBBLE_PREFERRED_WIDTH)), so a long registrable
domain is displayed with its TAIL deleted — the registry suffix, the
authoritative end of the identity — while the adjacent toggle is a live,
persistent STORAGE_ACCESS permission control. This violates Chromium’s URL
display guidelines (docs/security/url_display_guidelines/url_display_guidelines.md:
elide hostnames from the front; “Ensure that at least the registrable domain
can be shown”) and diverges from sibling trust surfaces that set ELIDE_HEAD
(e.g. chrome/browser/ui/views/web_apps/web_app_views_utils.cc: “Elide from head
to prevent origin spoofing”).
Observed rendering (65-char attacker eTLD+1
accounts-google-com-secure-signin-verification-gateway-portal.com):
displayed: [favicon] accounts-google-com-secure... [toggle]
hidden: -signin-verification-gateway-portal.com <- including ".com"
Only the attacker-authored, googley head survives; the disambiguating suffix a
wary user would check is unrecoverable at any bubble width (fixed 320px).
The same happens for PSL private-registry tenants (e.g.
<39-char-name>.github.io hides .github.io). Toggling the row persists
CONTENT_SETTING_ALLOW for ContentSettingsType::STORAGE_ACCESS with a
30-day lifetime (kStorageAccessAPIExplicitPermissionLifetime) — durable
unpartitioned cookie access for the attacker site under the current top-level
site.
Prerequisites
- Linux desktop Chromium build (verified against
out/asan/chrome; the bug is build-independent UI code). No source modification is needed to trigger it. - X display (run headful),
xdotool,scrot,python3,openssl. - Profile has the standard “Block third-party cookies” user setting
(
profile.cookie_controls_mode = 1; also the Incognito default). With third-party cookies allowed,requestStorageAccess()short-circuits tokAllowedByCookieSettingsand the Storage Access UI never engages — this setting is the mode the Storage Access UI is designed for, not a bypass.
Reproduction
cd <artifacts-dir>
./run_poc.sh /chromium_src/src/out/asan/chrome # ~2 minutes, fully scripted
The script drives real X11 input end-to-end:
- Serves both sites over HTTPS from 127.0.0.1:8443 (
server.py), with--host-resolver-rulesmapping the two domains to it:- attacker:
accounts-google-com-secure-signin-verification-gateway-portal.com(61-char label +.com= 65 chars — a registrable .com domain any attacker can buy) - victim top-level:
my-favorite-news-site.com
- attacker:
- Visits the attacker site top-level and clicks once (satisfies the Storage Access API’s BTM “top-level user interaction” heuristic), then the page redirects to the victim site, which embeds an attacker iframe.
- Clicks inside the iframe; the frame calls
document.requestStorageAccess()under transient activation → the Storage Access permission prompt appears (screenshot1_storage_access_prompt.png). - Clicks Block (
RequestOutcome::kDeniedByUser) → the Storage Access content-setting icon appears in the omnibox. - Clicks the omnibox icon →
ContentSettingBubbleContentsopens with the vulnerableContentSettingSiteRowView:2_bubble_site_row_tail_elided.png/2z_bubble_zoom.png— the site row readsaccounts-google-com-secure...with the toggle OFF; the registry suffix is not rendered. - Flips the toggle (what a user misjudging the truncated identity would do) —
3_toggle_flipped_on_elided_row.png/3z_bubble_zoom_toggled.png. - Closes the bubble (
WindowClosing→CommitChanges), quits Chrome, and verifies the persisted grant in the profile’sPreferences(result.txt).
Expected Result
- The bubble site row shows a tail-elided identity (
accounts-google-com-secure...) next to the live toggle;.com/ the true suffix is never displayed. - After the toggle + close, the profile persists a durable ALLOW:
Output
Actual output of run_poc.sh (result.txt):
persisted storage_access exceptions:
{
"https://[*.]accounts-google-com-secure-signin-verification-gateway-portal.com,https://[*.]my-favorite-news-site.com": {
"expiration": "13436220826588795",
"last_modified": "13433628826588840",
"lifetime": "2592000000000",
"setting": 1
}
}
PASS: durable CONTENT_SETTING_ALLOW persisted for the full 65-char eTLD+1
(the bubble rendered it tail-elided, without its registry suffix).
setting: 1 = CONTENT_SETTING_ALLOW; lifetime = 2,592,000,000,000 µs =
30 days = kStorageAccessAPIExplicitPermissionLifetime. The grant target is
the full 65-char eTLD+1 that the decision surface displayed as
accounts-google-com-secure....
Notes on fidelity / flags
All flags are environment plumbing only; none alter the vulnerable rendering or the storage-access decision logic:
--host-resolver-rules— DNS simulation; in production the attacker owns a real registered domain.--ignore-certificate-errors— stands in for the valid public-CA certificate the attacker would hold for their own domain (offline sandbox; nocertutilavailable to install a local trust anchor). Storage Access requires secure contexts, hence HTTPS.--no-first-run,--no-default-browser-check,--disable-search-engine-choice-screen,--user-data-dir,--no-proxy-server,--window-position/size,--ignore-gpu-blocklist— test-environment setup.ASAN_OPTIONS=detect_odr_violation=0— suppresses a benign component-ASAN-build startup artifact (duplicate vtable registration); irrelevant on non-ASAN builds.
Existing partial mitigations observed (do not fix the rendered decision surface): the toggle’s accessibility name carries the full string, and the label auto-tooltips the full text on hover. Neither is visible in the rendered row where the decision is made.
Prior art: this matches the accepted “Incorrect security UI in Permissions / domain spoofing” class (e.g. CVE-2026-5905, Chromium severity Low).
Fix (verified)
bandaid-fix.patch — one line in ContentSettingSiteRowView:
title_label->SetElideBehavior(gfx::ELIDE_HEAD);
matching the pattern used by other origin-displaying surfaces
(web_app_views_utils.cc). With the fix applied and rebuilt, the same POC run
renders the row as ...verification-gateway-portal.com — the head is elided
and the registrable-domain tail stays visible
(4_fixed_head_elision.png / 4z_fixed_zoom.png).
The same audit should cover the sibling Page Info two-site rows fed from the
same GetTwoSitePermissionRequesters data (components/page_info/page_info.cc)
and the Storage Access prompt title
(permission_prompt_bubble_two_origins_view.cc, tracked by
TODO(crbug.com/40064079)).
Files
run_poc.sh— end-to-end scripted reproduction (X11 input choreography)server.py,gen_cert.sh,www/landing.html,www/victim.html,www/frame.html— the two sitesscreenshots/— captured evidence (see above)result.txt— persisted-grant verification output of the recorded runbandaid-fix.patch— the verified one-line fix (not applied in-tree)
Attached file hierarchy:
The following directories were attached as zip archives:
screenshots.zip→screenshots/www.zip→www/
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.
- https://source.chromium.org/chromium/chromium/src/+/main:chrome/browser/ui/content_settings/content_setting_bubble_model.cc;l=850
- https://source.chromium.org/chromium/chromium/src/+/main:chrome/browser/ui/views/content_setting_bubble_contents.cc;l=382
- https://source.chromium.org/chromium/chromium/src/+/main:chrome/browser/ui/views/content_setting_site_row_view.cc;l=58
- https://source.chromium.org/chromium/chromium/src/+/main:chrome/browser/ui/views/content_setting_site_row_view.cc;l=61
- https://source.chromium.org/chromium/chromium/src/+/main:chrome/browser/ui/views/web_apps/web_app_views_utils.cc
- https://source.chromium.org/chromium/chromium/src/+/main:chrome/browser/ui/views/web_apps/web_app_views_utils.cc;l=51
- https://source.chromium.org/chromium/chromium/src/+/main:components/page_info/page_info.cc;l=1570
- https://source.chromium.org/chromium/chromium/src/+/main:components/url_formatter/elide_url.cc;l=351
- https://source.chromium.org/chromium/chromium/src/+/main:net/base/schemeful_site.cc;l=109
- https://source.chromium.org/chromium/chromium/src/+/main:ui/views/controls/label.h;l=542
- https://source.chromium.org/chromium/chromium/src/+/main:ui/views/layout/layout_provider.h;l=260