@@ -74,7 +96,8 @@ const streamingComponents = buildComponents(true)
* Render untrusted assistant-authored Markdown as semantic React elements.
* @param props - Markdown source text preserved by the session projection;
* `streaming` renders fences plain (highlighting lands on the finalize swap).
- * @returns A GFM document with raw HTML, relative links, unsafe protocols, and remote images disabled.
+ * @returns A GFM document with raw HTML, relative destinations, and unsafe
+ * protocols disabled; absolute HTTP(S) images render directly.
*/
export function MarkdownText({ text, streaming = false }: { text: string; streaming?: boolean }) {
return (
diff --git a/packages/client/ui-primitives/tests/markdown.spec.tsx b/packages/client/ui-primitives/tests/markdown.spec.tsx
index b7f665c78a..40295e16cb 100644
--- a/packages/client/ui-primitives/tests/markdown.spec.tsx
+++ b/packages/client/ui-primitives/tests/markdown.spec.tsx
@@ -94,13 +94,35 @@ describe('MarkdownText', () => {
expect(done.container.querySelector('pre.shiki')).not.toBeNull()
})
- it('neutralizes raw HTML, unsafe or relative links, and remote images', () => {
+ it('renders absolute HTTP(S) images with bounded presentation', () => {
+ const markdown = [
+ '',
+ '',
+ ].join('\n\n')
+ const { container } = render(
)
+ const images = [...container.querySelectorAll('img')]
+ expect(images.map(image => image.getAttribute('src'))).toEqual([
+ 'https://example.com/secure.png',
+ 'http://example.com/plain.png',
+ ])
+ for (const image of images) {
+ expect(image.getAttribute('loading')).toBe('lazy')
+ expect(image.getAttribute('decoding')).toBe('async')
+ expect(image.getAttribute('referrerpolicy')).toBe('no-referrer')
+ }
+ })
+
+ it('neutralizes raw HTML, unsafe or relative links, and unsupported images', () => {
const markdown = [
'',
'

',
'[script](javascript:alert(1)) [relative](/settings)',
'[mail](mailto:dev@example.com) [web](http://example.com) [upper](HTTPS://example.com)',
- '',
+ '',
+ '',
+ '',
+ ')',
+ '',
].join('\n\n')
const { container } = render(
)
@@ -112,7 +134,11 @@ describe('MarkdownText', () => {
expect(screen.getByRole('link', { name: 'mail' }).getAttribute('target')).toBeNull()
expect(screen.getByRole('link', { name: 'web' }).getAttribute('rel')).toBe('noopener noreferrer')
expect(screen.getByRole('link', { name: 'upper' }).getAttribute('target')).toBe('_blank')
- expect(screen.getByText('remote diagram')).toBeTruthy()
+ expect(screen.getByText('relative diagram')).toBeTruthy()
+ expect(screen.getByText('absolute diagram')).toBeTruthy()
+ expect(screen.getByText('file diagram')).toBeTruthy()
+ expect(screen.getByText('script diagram')).toBeTruthy()
+ expect(screen.getByText('mail diagram')).toBeTruthy()
})
it('keeps incomplete streaming Markdown renderable', () => {
diff --git a/tsconfig.host.json b/tsconfig.host.json
index b32c410961..795f5ae8d5 100644
--- a/tsconfig.host.json
+++ b/tsconfig.host.json
@@ -24,6 +24,7 @@
"apps/web/tests/code-mode-round.e2e.ts",
"apps/web/tests/cordis-tool-round.e2e.ts",
"apps/web/tests/message-actions.e2e.ts",
+ "apps/web/tests/markdown-images.e2e.ts",
"apps/web/tests/queue-actions.e2e.ts",
"apps/web/tests/skill-invocation-policy.e2e.ts",
"apps/cli/tests/**/*.ts",