Elide a long host from the left, as its own rule intends

The identity line is the only thing on the permission dialog naming the
requester, so a host too long for the dialog has to lose its front, not
its tail: the registrable domain is the part that says who is asking.

`direction: rtl` was there for that, but paired with
`unicode-bidi: plaintext` it does nothing — plaintext takes the base
direction from the content's own first strong character, which for any
Latin host is LTR, so the ellipsis went back on the right. Measured in
the real dialog, `account-security.paypal.com.verify-login.example`
rendered as `account-security.paypal.com.verify-l…`, reading as PayPal.

Isolating instead keeps the box anchored to the end of the text, and
still stops a bidi control character in the host from reordering
anything around it.

The test that covers this asserted the computed `direction` — the
property, not the outcome — so it passed throughout. It now measures
which characters are actually on screen, and that the host still reads
in source order.
This commit is contained in:
jelveh
2026-07-26 13:51:08 -07:00
parent e6fccf02a6
commit 19da9e11ab
2 changed files with 49 additions and 12 deletions
+12 -4
View File
@@ -6286,13 +6286,21 @@ html.dark-mode .usage-table-show-less:hover {
/* A hostname must never lose its tail to the ellipsis: the registrable domain
is the part that identifies who is asking. `direction: rtl` anchors the text
to its end, so a too-long host is elided from the left instead — what
browsers do in their own origin chips. Latin hosts still read left-to-right
because the content's own direction is resolved from its first strong
character. */
browsers do in their own origin chips. Latin hosts still read
left-to-right: their characters are strongly LTR, so the bidi algorithm
keeps them in order inside the right-anchored box.
`isolate`, not `plaintext`: plaintext takes the base direction from the
content's own first strong character, which for any Latin host overrides the
`direction` above and puts the ellipsis back on the right — hiding exactly
the part that identifies the requester, so
`accounts.google.com.attacker.example` reads as `accounts.google.com…`.
Isolating still keeps a bidi control character in the host from reordering
anything outside it. */
.perm-dialog-entity-host,
.perm-dialog-entity-origin {
direction: rtl;
unicode-bidi: plaintext;
unicode-bidi: isolate;
}
.perm-dialog-entity-origin {
@@ -1004,17 +1004,46 @@ test.describe('request-permission action hardening', () => {
const name = page.locator('dialog.perm-dialog .perm-dialog-entity-name');
await expect(name).toBeVisible({ timeout: 60_000 });
const info = await name.evaluate((el) => ({
text: el.textContent,
overflowing: el.scrollWidth > el.clientWidth,
direction: getComputedStyle(el).direction,
}));
// Measured from what is actually on screen, not from the declared
// `direction`: `unicode-bidi: plaintext` leaves `direction: rtl`
// computing as `rtl` while taking the real base direction from the
// content's first strong character, which for any Latin host put the
// ellipsis back on the right. Asserting the property passed while the
// rendering did the opposite of what it claims.
const info = await name.evaluate((el) => {
const node = el.firstChild;
const box = el.getBoundingClientRect();
const rectOf = (i) => {
const r = document.createRange();
r.setStart(node, i);
r.setEnd(node, i + 1);
return r.getBoundingClientRect();
};
const text = node.textContent;
const visible = [];
for ( let i = 0; i < text.length; i++ ) {
const r = rectOf(i);
if ( r.left >= box.left - 1 && r.right <= box.right + 1 ) {
visible.push({ ch: text[i], i, left: r.left });
}
}
return {
text,
overflowing: el.scrollWidth > el.clientWidth,
visibleText: visible.map((c) => c.ch).join(''),
// Still reads left-to-right, in source order: anchoring the box
// to its end must not reorder the host itself.
readsInOrder: visible.every((c, k) => k === 0
|| (c.i > visible[k - 1].i && c.left >= visible[k - 1].left)),
};
});
// The host is genuinely too long for the dialog, so the elision this
// asserts about is actually happening.
expect(info.text).toBe(host);
expect(info.overflowing).toBe(true);
// `direction: rtl` anchors the text to its end, so the ellipsis lands on
// the left and the registrable domain stays on screen.
expect(info.direction).toBe('rtl');
// What survives the ellipsis is the end of the host — the part that says
// who is really asking — not the trusted-looking prefix.
expect(info.visibleText.endsWith('attacker-run-domain.example')).toBe(true);
expect(info.readsInOrder).toBe(true);
});
});