Skip to content

[chat] Message part URLs used as link href without sanitization (javascript: XSS on React 17/18) #22726

Description

@Anexus5919

The problem in depth

The default chat message-part renderers in @mui/x-chat-headless write the untrusted part.url straight into an anchor href with no sanitization. Message parts come from assistant/model output, tool results, or RAG source data, so a javascript: (or data:) URL flows unmodified into the link and becomes clickable.

Affected sinks:

  • packages/x-chat-headless/src/message/defaultMessagePartRenderers.tsx (renderDefaultFilePart non-image anchor, and renderDefaultSourceUrlPart anchor)
  • packages/x-chat-headless/src/message/parts/SourceUrlPart.tsx
  • packages/x-chat-headless/src/message/parts/FilePart.tsx

The package already ships a sanitizer, safeUri() in packages/x-chat-headless/src/message/parts/partUtils.ts, which whitelists http/https/mailto/tel and is unit-tested, but none of the renderers call it.

React version matters here, and the package supports react "^17.0.0 || ^18.0.0 || ^19.0.0":

  • React 17 and 18 render javascript: URLs as-is, so the link executes the script on click.
  • React 19 neutralizes javascript: URLs at render time, so that specific vector does not fire there.
  • No React version blocks data: URLs at render time.

So apps on React 17 or 18 are exposed, and relying on a React 19 only protection is fragile when the package advertises React 17/18 support. safeUri also closes the data: vector at the library level.

Steps to reproduce

Standalone proof of how the URL reaches the DOM on each supported React version:

const React = require('react');
const { renderToStaticMarkup } = require('react-dom/server');

console.log(
  renderToStaticMarkup(
    React.createElement('a', { href: 'javascript:alert(document.cookie)' }, 'Click me'),
  ),
);
// React 17.0.2 / 18.3.1: <a href="javascript:alert(document.cookie)">Click me</a>  (rendered as-is)
// React 19.x:            href replaced by React with a harmless throw sentinel

In the component, on a React 17 or 18 app:

  1. Render a chat with the default renderers (ChatRoot + MessageRoot + MessageContent).
  2. Provide an assistant message with a source-url part:
    { type: 'source-url', sourceId: 's1', url: 'javascript:alert(document.cookie)', title: 'Click me' }
  3. The rendered anchor is <a href="javascript:alert(document.cookie)">Click me</a>. Clicking it runs the script in the app origin.

A file part with a javascript: url reproduces the same through its anchor.

Current behavior

part.url is used directly as the href of the rendered anchor in the three files above, with no sanitization. On React 17/18 a javascript: URL is clickable and executes.

Expected behavior

Untrusted message-part URLs should be sanitized before being used as a link href, independent of the host React version. A javascript:/data: URL should produce an inert link. The existing safeUri() helper already implements the right policy.

Context

Building a chat UI with the @mui/x-chat-headless default renderers and showing model/tool/RAG provided source links. The suggested fix is to wrap part.url with the existing safeUri() in the anchor href of the three renderers. Image src should be left unchanged so legitimate data:image and blob: previews keep working, and because a javascript: value in <img src> does not execute.

Your environment

Reproduced against the package source on the current master, and verified how the URL reaches the DOM with react-dom/server renderToStaticMarkup on React 17.0.2, 18.3.1, and 19.2.7.

@mui/x-chat-headless: current master
react / react-dom: 17.0.2, 18.3.1, 19.2.7

Search keywords: chat, x-chat, x-chat-headless, sanitize, javascript URL, href, safeUri, message part, source-url, XSS

Metadata

Metadata

Assignees

No one assigned

    Labels

    scope: chatChanges related to the AI chat.staleInactive for 7 days (issues) or 30 days (PRs); closed after 5 or 15 more days if no update.status: waiting for maintainerThese issues haven't been looked at yet by a maintainer.type: enhancementIt’s an improvement, but we can’t make up our mind whether it's a bug fix or a new feature.
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions