Low chrome Memory Corruption 📄 Reporter bug report 🔧 Commit mapped

Overview

Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactMemory corruption in Browser
DescriptionMemory corruption in Browser
ComponentBrowser
Sub-componentContent Settings
Bug ClassMemory Corruption
Tracker562962437
Fix commitcbdaf1b9bfdc (chromium/src) +4/-0
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-20

Files Changed

  • chrome/browser/ui/views/content_setting_site_row_view.cc
From cbdaf1b9bfdcb018b00555aaccf9df16888acbf1 Mon Sep 17 00:00:00 2001
From: Dana Fried <[email protected]>
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 <[email protected]>
Reviewed-by: Foromo Daniel Soromou <[email protected]>
Commit-Queue: Dana Fried <[email protected]>
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,
Loading diff…

Original Bug Report

reported by [email protected]

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:

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

  1. 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 via FormatUrlForSecurityDisplay, which applies no length cap (components/url_formatter/elide_url.cc:351-393).
  2. Label defaults to tail elision: content_setting_site_row_view.cc:61-68 — no SetElideBehavior() call, so the default gfx::ELIDE_TAIL applies:
    // [`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)
    
  3. 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.
  4. 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) writes CONTENT_SETTING_ALLOW for ContentSettingsType::STORAGE_ACCESS with kStorageAccessAPIExplicitPermissionLifetime (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):

  1. 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).
  2. 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.
  3. Victim clicks the omnibox icon: the bubble lists the attacker site as accounts-google-com-secure... next to the toggle.
  4. Victim flips the toggle and the bubble closes.
  5. 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 to kAllowedByCookieSettings and 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:

  1. Serves both sites over HTTPS from 127.0.0.1:8443 (server.py), with --host-resolver-rules mapping 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
  2. 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.
  3. Clicks inside the iframe; the frame calls document.requestStorageAccess() under transient activation → the Storage Access permission prompt appears (screenshot 1_storage_access_prompt.png).
  4. Clicks Block (RequestOutcome::kDeniedByUser) → the Storage Access content-setting icon appears in the omnibox.
  5. Clicks the omnibox icon → ContentSettingBubbleContents opens with the vulnerable ContentSettingSiteRowView: 2_bubble_site_row_tail_elided.png / 2z_bubble_zoom.png — the site row reads accounts-google-com-secure... with the toggle OFF; the registry suffix is not rendered.
  6. Flips the toggle (what a user misjudging the truncated identity would do) — 3_toggle_flipped_on_elided_row.png / 3z_bubble_zoom_toggled.png.
  7. Closes the bubble (WindowClosing → CommitChanges), quits Chrome, and verifies the persisted grant in the profile’s Preferences (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; no certutil available 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 sites
  • screenshots/ — captured evidence (see above)
  • result.txt — persisted-grant verification output of the recorded run
  • bandaid-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.

View on issue tracker