From e8cf596a3a0f21048652bbbfac6a078169727d96 Mon Sep 17 00:00:00 2001 From: Helder Oliveira Date: Wed, 15 Apr 2026 15:24:37 +0100 Subject: [PATCH 1/7] =?UTF-8?q?fix:=20=F0=9F=90=9B=20Add=20URL=20validatio?= =?UTF-8?q?n=20and=20noopener=20to=20CardHorizontal?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Validate infoUrl starts with http:// or https:// to prevent javascript: URI attacks - Add noopener,noreferrer to window.open for security --- src/components/CardHorizontal/CardHorizontal.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/CardHorizontal/CardHorizontal.tsx b/src/components/CardHorizontal/CardHorizontal.tsx index d00628c61..f7be44361 100644 --- a/src/components/CardHorizontal/CardHorizontal.tsx +++ b/src/components/CardHorizontal/CardHorizontal.tsx @@ -204,8 +204,8 @@ export const CardHorizontal = ({ if (typeof onButtonClick === 'function') { onButtonClick(e); } - if (infoUrl && infoUrl.length > 0) { - window.open(infoUrl, '_blank'); + if (infoUrl && infoUrl.length > 0 && /^https?:\/\//i.test(infoUrl)) { + window.open(infoUrl, '_blank', 'noopener,noreferrer'); } }; return ( From 37d7637b8fc2564dc827e7452adb10777cb4f30d Mon Sep 17 00:00:00 2001 From: Helder Oliveira Date: Wed, 15 Apr 2026 15:25:12 +0100 Subject: [PATCH 2/7] =?UTF-8?q?chore:=20=F0=9F=A4=96=20Add=20changeset=20f?= =?UTF-8?q?or=20CardHorizontal=20security=20fix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changeset/fix-card-horizontal-security.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/fix-card-horizontal-security.md diff --git a/.changeset/fix-card-horizontal-security.md b/.changeset/fix-card-horizontal-security.md new file mode 100644 index 000000000..8d990460e --- /dev/null +++ b/.changeset/fix-card-horizontal-security.md @@ -0,0 +1,5 @@ +--- +"@clickhouse/click-ui": patch +--- + +Security fix for CardHorizontal: validate URLs before opening and add noopener/noreferrer. Prevents javascript: URI attacks and tab-nabbing vulnerabilities. From 89f1c4f80892a27ea6113c2ed35c4258e8e2f688 Mon Sep 17 00:00:00 2001 From: Helder Oliveira Date: Wed, 15 Apr 2026 15:27:31 +0100 Subject: [PATCH 3/7] =?UTF-8?q?chore:=20=F0=9F=A4=96=20update=20changeset?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changeset/fix-card-horizontal-security.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/fix-card-horizontal-security.md b/.changeset/fix-card-horizontal-security.md index 8d990460e..1a4c216f7 100644 --- a/.changeset/fix-card-horizontal-security.md +++ b/.changeset/fix-card-horizontal-security.md @@ -2,4 +2,4 @@ "@clickhouse/click-ui": patch --- -Security fix for CardHorizontal: validate URLs before opening and add noopener/noreferrer. Prevents javascript: URI attacks and tab-nabbing vulnerabilities. +Security fix for CardHorizontal by validating URLs before opening. Also, adds `noopener/noreferrer`. To help prevents javascript URI and tab related attacks. From e0d111f7a0c0a3732ac7e04eb427aa668f3820fe Mon Sep 17 00:00:00 2001 From: Helder Oliveira Date: Wed, 15 Apr 2026 15:29:41 +0100 Subject: [PATCH 4/7] =?UTF-8?q?refactor:=20=F0=9F=92=A1=20Use=20URL=20API?= =?UTF-8?q?=20for=20validation=20in=20CardHorizontal?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace regex with native URL API for more robust validation - Export isValidHttpUrl utility function - Add unit tests for URL validation --- .../CardHorizontal/CardHorizontal.test.tsx | 35 ++++++++++++++++++- .../CardHorizontal/CardHorizontal.tsx | 9 +++-- .../CardHorizontal/CardHorizontal.types.ts | 9 +++++ src/components/CardHorizontal/index.ts | 1 + 4 files changed, 51 insertions(+), 3 deletions(-) diff --git a/src/components/CardHorizontal/CardHorizontal.test.tsx b/src/components/CardHorizontal/CardHorizontal.test.tsx index 38f14a619..77e8d40b6 100644 --- a/src/components/CardHorizontal/CardHorizontal.test.tsx +++ b/src/components/CardHorizontal/CardHorizontal.test.tsx @@ -1,5 +1,9 @@ import { screen } from '@testing-library/react'; -import { CardHorizontal, CardHorizontalProps } from '@/components/CardHorizontal'; +import { + CardHorizontal, + CardHorizontalProps, + isValidHttpUrl, +} from '@/components/CardHorizontal'; import { renderCUI } from '@/utils/test-utils'; describe('CardHorizontal Component', () => { @@ -188,3 +192,32 @@ describe('CardHorizontal Component', () => { windowOpenSpy.mockRestore(); }); }); + +describe('isValidHttpUrl', () => { + it('should return true for valid HTTP URLs', () => { + expect(isValidHttpUrl('http://example.com')).toBe(true); + expect(isValidHttpUrl('http://localhost:3000')).toBe(true); + expect(isValidHttpUrl('http://test.com/path')).toBe(true); + }); + + it('should return true for valid HTTPS URLs', () => { + expect(isValidHttpUrl('https://example.com')).toBe(true); + expect(isValidHttpUrl('https://clickhouse.com')).toBe(true); + expect(isValidHttpUrl('https://api.example.com/v1/test')).toBe(true); + }); + + it('should return false for invalid URLs', () => { + expect(isValidHttpUrl('javascript:alert(1)')).toBe(false); + expect(isValidHttpUrl('data:text/html,')).toBe(false); + expect(isValidHttpUrl('file:///etc/passwd')).toBe(false); + expect(isValidHttpUrl('ftp://example.com')).toBe(false); + expect(isValidHttpUrl('about:blank')).toBe(false); + expect(isValidHttpUrl('vbscript:msgbox(1)')).toBe(false); + }); + + it('should return false for non-HTTP protocols', () => { + expect(isValidHttpUrl('')).toBe(false); + expect(isValidHttpUrl('not-a-url')).toBe(false); + expect(isValidHttpUrl('://invalid')).toBe(false); + }); +}); diff --git a/src/components/CardHorizontal/CardHorizontal.tsx b/src/components/CardHorizontal/CardHorizontal.tsx index f7be44361..a29ad7ea1 100644 --- a/src/components/CardHorizontal/CardHorizontal.tsx +++ b/src/components/CardHorizontal/CardHorizontal.tsx @@ -3,7 +3,12 @@ import { Badge } from '@/components/Badge'; import { Button } from '@/components/Button'; import { Container } from '@/components/Container'; import { Icon } from '@/components/Icon'; -import { CardHorizontalProps, CardSize, CardColor } from './CardHorizontal.types'; +import { + CardHorizontalProps, + CardSize, + CardColor, + isValidHttpUrl, +} from './CardHorizontal.types'; const Header = styled.div` max-width: 100%; @@ -204,7 +209,7 @@ export const CardHorizontal = ({ if (typeof onButtonClick === 'function') { onButtonClick(e); } - if (infoUrl && infoUrl.length > 0 && /^https?:\/\//i.test(infoUrl)) { + if (infoUrl && infoUrl.length > 0 && isValidHttpUrl(infoUrl)) { window.open(infoUrl, '_blank', 'noopener,noreferrer'); } }; diff --git a/src/components/CardHorizontal/CardHorizontal.types.ts b/src/components/CardHorizontal/CardHorizontal.types.ts index 5eb228d2b..a2c24f272 100644 --- a/src/components/CardHorizontal/CardHorizontal.types.ts +++ b/src/components/CardHorizontal/CardHorizontal.types.ts @@ -5,6 +5,15 @@ import type { BadgeState } from '@/components/Badge'; export type CardColor = 'default' | 'muted'; export type CardSize = 'sm' | 'md'; +export const isValidHttpUrl = (url: string): boolean => { + try { + const parsedUrl = new URL(url); + return parsedUrl.protocol === 'http:' || parsedUrl.protocol === 'https:'; + } catch { + return false; + } +}; + export interface CardHorizontalProps extends Omit< HTMLAttributes, 'title' diff --git a/src/components/CardHorizontal/index.ts b/src/components/CardHorizontal/index.ts index de289f469..36d0f4a1b 100644 --- a/src/components/CardHorizontal/index.ts +++ b/src/components/CardHorizontal/index.ts @@ -1,2 +1,3 @@ export { CardHorizontal } from './CardHorizontal'; +export { isValidHttpUrl } from './CardHorizontal.types'; export type { CardSize, CardHorizontalProps } from './CardHorizontal.types'; From 433f19df1017a779ca0078405f7ee82877ff511a Mon Sep 17 00:00:00 2001 From: Helder Oliveira Date: Wed, 15 Apr 2026 15:33:49 +0100 Subject: [PATCH 5/7] =?UTF-8?q?chore:=20=F0=9F=A4=96=20Add=20MDN=20referen?= =?UTF-8?q?ce=20to=20CardHorizontal=20security=20changeset?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changeset/fix-card-horizontal-security.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.changeset/fix-card-horizontal-security.md b/.changeset/fix-card-horizontal-security.md index 1a4c216f7..e44075bb8 100644 --- a/.changeset/fix-card-horizontal-security.md +++ b/.changeset/fix-card-horizontal-security.md @@ -2,4 +2,12 @@ "@clickhouse/click-ui": patch --- -Security fix for CardHorizontal by validating URLs before opening. Also, adds `noopener/noreferrer`. To help prevents javascript URI and tab related attacks. +Improved how the `CardHorizontal` component handles external links to keep end-users safe. + +### What's changed +- **Link validation**: Only secure web links (starting with `http://` or `https://`) can be opened. This blocks potentially harmful links like `javascript:` or `data:` URIs that could be used in attacks. + +- **Safer tab opening**: When a link opens in a new tab, it now uses `noopener,noreferrer` to prevent the new page from accessing information about your application. See [MDN: Window.open() - Security considerations](https://developer.mozilla.org/en-US/docs/Web/API/Window/open#security_considerations) for more details. + +### What this means for you +No action needed! The `CardHorizontal` component now automatically protects users when they click info links. All existing cards will benefit from these security improvements without any code changes. From 490f5b65024185879cddd37cc87ed6bfb09e0c61 Mon Sep 17 00:00:00 2001 From: Helder Oliveira Date: Wed, 15 Apr 2026 15:40:32 +0100 Subject: [PATCH 6/7] =?UTF-8?q?chore:=20=F0=9F=A4=96=20update=20changeset?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changeset/fix-card-horizontal-security.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/.changeset/fix-card-horizontal-security.md b/.changeset/fix-card-horizontal-security.md index e44075bb8..62f1e63b9 100644 --- a/.changeset/fix-card-horizontal-security.md +++ b/.changeset/fix-card-horizontal-security.md @@ -8,6 +8,3 @@ Improved how the `CardHorizontal` component handles external links to keep end-u - **Link validation**: Only secure web links (starting with `http://` or `https://`) can be opened. This blocks potentially harmful links like `javascript:` or `data:` URIs that could be used in attacks. - **Safer tab opening**: When a link opens in a new tab, it now uses `noopener,noreferrer` to prevent the new page from accessing information about your application. See [MDN: Window.open() - Security considerations](https://developer.mozilla.org/en-US/docs/Web/API/Window/open#security_considerations) for more details. - -### What this means for you -No action needed! The `CardHorizontal` component now automatically protects users when they click info links. All existing cards will benefit from these security improvements without any code changes. From 0b97e3013170217d6270e174732196f65701056a Mon Sep 17 00:00:00 2001 From: Helder Oliveira Date: Wed, 15 Apr 2026 15:42:29 +0100 Subject: [PATCH 7/7] =?UTF-8?q?fix:=20=F0=9F=90=9B=20move=20validation=20u?= =?UTF-8?q?tl=20to=20utils?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/CardHorizontal/CardHorizontal.test.tsx | 7 ++----- src/components/CardHorizontal/CardHorizontal.tsx | 8 ++------ src/components/CardHorizontal/CardHorizontal.types.ts | 9 --------- src/components/CardHorizontal/index.ts | 1 - src/utils/url.ts | 8 ++++++++ 5 files changed, 12 insertions(+), 21 deletions(-) create mode 100644 src/utils/url.ts diff --git a/src/components/CardHorizontal/CardHorizontal.test.tsx b/src/components/CardHorizontal/CardHorizontal.test.tsx index 77e8d40b6..2c356161f 100644 --- a/src/components/CardHorizontal/CardHorizontal.test.tsx +++ b/src/components/CardHorizontal/CardHorizontal.test.tsx @@ -1,9 +1,6 @@ import { screen } from '@testing-library/react'; -import { - CardHorizontal, - CardHorizontalProps, - isValidHttpUrl, -} from '@/components/CardHorizontal'; +import { CardHorizontal, CardHorizontalProps } from '@/components/CardHorizontal'; +import { isValidHttpUrl } from '@/utils/url'; import { renderCUI } from '@/utils/test-utils'; describe('CardHorizontal Component', () => { diff --git a/src/components/CardHorizontal/CardHorizontal.tsx b/src/components/CardHorizontal/CardHorizontal.tsx index a29ad7ea1..52a8faab7 100644 --- a/src/components/CardHorizontal/CardHorizontal.tsx +++ b/src/components/CardHorizontal/CardHorizontal.tsx @@ -3,12 +3,8 @@ import { Badge } from '@/components/Badge'; import { Button } from '@/components/Button'; import { Container } from '@/components/Container'; import { Icon } from '@/components/Icon'; -import { - CardHorizontalProps, - CardSize, - CardColor, - isValidHttpUrl, -} from './CardHorizontal.types'; +import { CardHorizontalProps, CardSize, CardColor } from './CardHorizontal.types'; +import { isValidHttpUrl } from '@/utils/url'; const Header = styled.div` max-width: 100%; diff --git a/src/components/CardHorizontal/CardHorizontal.types.ts b/src/components/CardHorizontal/CardHorizontal.types.ts index a2c24f272..5eb228d2b 100644 --- a/src/components/CardHorizontal/CardHorizontal.types.ts +++ b/src/components/CardHorizontal/CardHorizontal.types.ts @@ -5,15 +5,6 @@ import type { BadgeState } from '@/components/Badge'; export type CardColor = 'default' | 'muted'; export type CardSize = 'sm' | 'md'; -export const isValidHttpUrl = (url: string): boolean => { - try { - const parsedUrl = new URL(url); - return parsedUrl.protocol === 'http:' || parsedUrl.protocol === 'https:'; - } catch { - return false; - } -}; - export interface CardHorizontalProps extends Omit< HTMLAttributes, 'title' diff --git a/src/components/CardHorizontal/index.ts b/src/components/CardHorizontal/index.ts index 36d0f4a1b..de289f469 100644 --- a/src/components/CardHorizontal/index.ts +++ b/src/components/CardHorizontal/index.ts @@ -1,3 +1,2 @@ export { CardHorizontal } from './CardHorizontal'; -export { isValidHttpUrl } from './CardHorizontal.types'; export type { CardSize, CardHorizontalProps } from './CardHorizontal.types'; diff --git a/src/utils/url.ts b/src/utils/url.ts new file mode 100644 index 000000000..876000e2b --- /dev/null +++ b/src/utils/url.ts @@ -0,0 +1,8 @@ +export const isValidHttpUrl = (url: string): boolean => { + try { + const parsedUrl = new URL(url); + return parsedUrl.protocol === 'http:' || parsedUrl.protocol === 'https:'; + } catch { + return false; + } +};