From a2e0636c1f53010352b8476303455c1f497159f9 Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Mon, 20 Apr 2026 21:22:20 +0100 Subject: [PATCH] fix(ui): address PR review comments on Header components - Guard against unsafe URL schemes (javascript:/vbscript:/data:) in description links - Use index-based keys for tags and metadata to avoid duplicate-key warnings - Render ReactNode metadata values directly instead of wrapping in Text to avoid invalid span>div nesting; only wrap plain strings in Text - Replace empty-string Avatar src fallback with data:, to prevent spurious page requests - Fix JSDoc in HeaderMetadataUsers to accurately describe the row layout Signed-off-by: Charles de Dreuille --- packages/ui/src/components/Header/Header.tsx | 32 +++++++++++++------ .../components/Header/HeaderMetadataUsers.tsx | 8 ++--- 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/packages/ui/src/components/Header/Header.tsx b/packages/ui/src/components/Header/Header.tsx index 9bd8bcdeab..67be3eb6bd 100644 --- a/packages/ui/src/components/Header/Header.tsx +++ b/packages/ui/src/components/Header/Header.tsx @@ -24,10 +24,14 @@ import { Link } from '../Link'; import { Fragment } from 'react/jsx-runtime'; const INLINE_LINK_RE = /\[([^\]]+)\]\(([^)]+)\)/g; +// Reject javascript:/vbscript:/data: URIs to prevent XSS via description links. +const UNSAFE_HREF_RE = /^(javascript:|vbscript:|data:)/i; /** * Renders a plain-text string that may contain inline Markdown links of the * form `[label](href)` as an array of React nodes (strings and Link elements). + * Links with unsafe URL schemes (javascript:, vbscript:, data:) are rendered + * as plain text instead. * * We intentionally avoid `react-markdown` here: that package is ESM-only * (v8+), which breaks Jest in Node-role packages that transitively import @@ -44,11 +48,17 @@ function renderInlineMarkdown(text: string): React.ReactNode[] { if (match.index > last) { parts.push(text.slice(last, match.index)); } - parts.push( - - {match[1]} - , - ); + const href = match[2]; + const label = match[1]; + if (UNSAFE_HREF_RE.test(href)) { + parts.push(label); + } else { + parts.push( + + {label} + , + ); + } last = match.index + match[0].length; } if (last < text.length) { @@ -81,7 +91,7 @@ export const Header = (props: HeaderProps) => { {tags && tags.length > 0 && (
{tags.map((tag, i) => ( - + {i > 0 && } {tag.href ? ( { )} {metadata && metadata.length > 0 && (
- {metadata.map(item => ( -
+ {metadata.map((item, i) => ( +
{item.label} - {item.value} + {typeof item.value === 'string' ? ( + {item.value} + ) : ( + item.value + )}
))}
diff --git a/packages/ui/src/components/Header/HeaderMetadataUsers.tsx b/packages/ui/src/components/Header/HeaderMetadataUsers.tsx index 3f68398ca5..e644811899 100644 --- a/packages/ui/src/components/Header/HeaderMetadataUsers.tsx +++ b/packages/ui/src/components/Header/HeaderMetadataUsers.tsx @@ -25,7 +25,7 @@ import styles from './HeaderMetadataUsers.module.css'; /** * Displays a list of users as avatars inside a Header metadata value. * A single user shows the avatar with their name beside it. - * Multiple users show overlapping avatars with the name revealed on hover via tooltip. + * Multiple users show avatars in a row with the name revealed on hover via tooltip. * When a user has an `href`, the avatar and name become links. * * @public @@ -41,7 +41,7 @@ export const HeaderMetadataUsers = ({ const user = users[0]; const avatar = (