From cffd8cfd70539720b70c0fddd03717232942433b Mon Sep 17 00:00:00 2001 From: Will Hunt <2072976+Half-Shot@users.noreply.github.com> Date: Tue, 7 Apr 2026 08:53:06 +0100 Subject: [PATCH] Disallow links without protocol (e.g. starting with http(s)://) in LinkedText. (#32972) * Disallow links without protocols in LinkedText. * Update tests --- apps/web/playwright/e2e/links/messages.spec.ts | 9 ++++++++- apps/web/playwright/e2e/links/topic.spec.ts | 8 +------- .../src/core/utils/LinkedText/LinkedText.test.tsx | 11 ++++++++++- packages/shared-components/src/core/utils/linkify.ts | 4 ++-- 4 files changed, 21 insertions(+), 11 deletions(-) diff --git a/apps/web/playwright/e2e/links/messages.spec.ts b/apps/web/playwright/e2e/links/messages.spec.ts index 1f86e1bc09..74af6c834e 100644 --- a/apps/web/playwright/e2e/links/messages.spec.ts +++ b/apps/web/playwright/e2e/links/messages.spec.ts @@ -14,7 +14,7 @@ test.describe("Message links", () => { await use({ roomId }); }, }); - for (const link of ["https://example.org", "example.org", "ftp://example.org"]) { + for (const link of ["https://example.org", "ftp://example.org"]) { test(`should linkify a regular link '${link}'`, async ({ page, user, app, room }) => { await page.goto(`#/room/${room.roomId}`); // Needs to be unformatted so we test linkifing @@ -24,6 +24,13 @@ test.describe("Message links", () => { await expect(linkElement).toBeVisible(); }); } + test("should NOT linkify a bare domain", async ({ page, user, app, room }) => { + await page.goto(`#/room/${room.roomId}`); + // Needs to be unformatted so we test linkifing + await app.client.sendMessage(room.roomId, `Check out example.org`); + const linkElement = page.locator(".mx_EventTile_last").getByRole("link", { name: "example.org" }); + await expect(linkElement).not.toBeVisible(); + }); test("should linkify a User ID", async ({ page, user, app, room }) => { await page.goto(`#/room/${room.roomId}`); // Needs to be unformatted so we test linkifing diff --git a/apps/web/playwright/e2e/links/topic.spec.ts b/apps/web/playwright/e2e/links/topic.spec.ts index f6574860cd..fff866119d 100644 --- a/apps/web/playwright/e2e/links/topic.spec.ts +++ b/apps/web/playwright/e2e/links/topic.spec.ts @@ -14,13 +14,7 @@ test.describe("Topic links", () => { await use({ roomId }); }, }); - for (const link of [ - "https://example.org", - "example.org", - "ftp://example.org", - "#aroom:example.org", - "@alice:example.org", - ]) { + for (const link of ["https://example.org", "ftp://example.org", "#aroom:example.org", "@alice:example.org"]) { // Playwright treats '@' as a tag, so replace it to be safe test(`should linkify plaintext '${link.replace("@", "_@")}'`, async ({ page, user, app, room }) => { await app.client.sendStateEvent( diff --git a/packages/shared-components/src/core/utils/LinkedText/LinkedText.test.tsx b/packages/shared-components/src/core/utils/LinkedText/LinkedText.test.tsx index 850bcb0c15..fb6c8e26f4 100644 --- a/packages/shared-components/src/core/utils/LinkedText/LinkedText.test.tsx +++ b/packages/shared-components/src/core/utils/LinkedText/LinkedText.test.tsx @@ -49,6 +49,15 @@ describe("LinkedText", () => { expect(container).toMatchSnapshot(); }); + it("does not linkify domains without a protocol.", () => { + const { queryAllByRole } = render( + + Check out this link github.com + , + ); + expect(queryAllByRole("link")).toHaveLength(0); + }); + it("renders a user ID", () => { const { container } = render(); expect(container).toMatchSnapshot(); @@ -73,7 +82,7 @@ describe("LinkedText", () => { const fn = vitest.fn(); const { getAllByRole } = render( - Check out this link https://google.com and example.org + Check out this link https://google.com and https://example.org , ); const links = getAllByRole("link"); diff --git a/packages/shared-components/src/core/utils/linkify.ts b/packages/shared-components/src/core/utils/linkify.ts index 7ff737c2cb..c4d5caeea9 100644 --- a/packages/shared-components/src/core/utils/linkify.ts +++ b/packages/shared-components/src/core/utils/linkify.ts @@ -228,10 +228,10 @@ export function generateLinkedTextOptions({ : undefined), // By default, ignore Matrix ID types. // Other applications may implement their own version of LinkifyComponent. - validate: (_value, type: string) => + validate: (value, type: string) => !!(type === LinkifyMatrixOpaqueIdType.UserId && userIdListener) || !!(type === LinkifyMatrixOpaqueIdType.RoomAlias && roomAliasListener) || - type === LinkifyMatrixOpaqueIdType.URL, + !!(type === LinkifyMatrixOpaqueIdType.URL && URL.canParse(value)), } satisfies linkifyjs.Opts; }