VYPR
Moderate severityNVD Advisory· Published Sep 18, 2025· Updated Sep 19, 2025

Lobe Chat Desktop Vulnerable to Remote Code Execution via XSS in Chat Messages

CVE-2025-59417

Description

Lobe Chat is an open-source artificial intelligence chat framework. Prior to version 1.129.4, there is a a cross-site scripting (XSS) vulnerability when handling chat message in lobe-chat that can be escalated to remote code execution on the user’s machine. In lobe-chat, when the response from the server is like <lobeArtifact identifier="ai-new-interpretation" ...> , it will be rendered with the lobeArtifact node, instead of the plain text. However, when the type of the lobeArtifact is image/svg+xml , it will be rendered as the SVGRender component, which internally uses dangerouslySetInnerHTML to set the content of the svg, resulting in XSS attack. Any party capable of injecting content into chat messages, such as hosting a malicious page for prompt injection, operating a compromised MCP server, or leveraging tool integrations, can exploit this vulnerability. This vulnerability is fixed in 1.129.4.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
@lobehub/chatnpm
< 1.129.41.129.4

Affected products

1

Patches

1
9f044edd07ce

🐛 fix: fix svg xss issue (#9313)

https://github.com/lobehub/lobe-chatArvin XuSep 18, 2025via ghsa
6 files changed · +153 4
  • packages/utils/package.json+3 1 modified
    @@ -14,9 +14,11 @@
       "dependencies": {
         "@lobechat/const": "workspace:*",
         "@lobechat/types": "workspace:*",
    -    "dayjs": "^1.11.18"
    +    "dayjs": "^1.11.18",
    +    "dompurify": "^3.2.7"
       },
       "devDependencies": {
    +    "@types/dompurify": "^3.2.0",
         "vitest-canvas-mock": "^0.3.3"
       }
     }
    
  • packages/utils/src/client/clipboard.ts+0 0 renamed
  • packages/utils/src/client/index.ts+2 0 modified
    @@ -1,2 +1,4 @@
    +export * from './clipboard';
     export * from './downloadFile';
     export * from './exportFile';
    +export * from './sanitize';
    
  • packages/utils/src/client/sanitize.test.ts+108 0 added
    @@ -0,0 +1,108 @@
    +import { describe, expect, it } from 'vitest';
    +
    +import { sanitizeSVGContent } from './sanitize';
    +
    +describe('sanitizeSVGContent', () => {
    +  it('should preserve safe SVG elements and attributes', () => {
    +    const safeSvg = `
    +      <svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100">
    +        <circle cx="50" cy="50" r="40" fill="red" stroke="blue" stroke-width="2" />
    +        <rect x="10" y="10" width="30" height="30" fill="green" />
    +        <path d="M10,20 L30,40" stroke="black" />
    +      </svg>
    +    `;
    +
    +    const sanitized = sanitizeSVGContent(safeSvg);
    +
    +    expect(sanitized).toContain('<svg');
    +    expect(sanitized).toContain('xmlns="http://www.w3.org/2000/svg"');
    +    expect(sanitized).toContain('<circle');
    +    expect(sanitized).toContain('fill="red"');
    +    expect(sanitized).toContain('<rect');
    +    expect(sanitized).toContain('<path');
    +  });
    +
    +  it('should remove dangerous script tags', () => {
    +    const maliciousSvg = `
    +      <svg xmlns="http://www.w3.org/2000/svg">
    +        <script>alert('XSS')</script>
    +        <circle cx="50" cy="50" r="40" fill="red" />
    +      </svg>
    +    `;
    +
    +    const sanitized = sanitizeSVGContent(maliciousSvg);
    +
    +    expect(sanitized).not.toContain('<script>');
    +    expect(sanitized).not.toContain('alert');
    +    expect(sanitized).toContain('<svg');
    +  });
    +
    +  it('should remove dangerous event handler attributes', () => {
    +    const maliciousSvg = `
    +      <svg xmlns="http://www.w3.org/2000/svg">
    +        <circle cx="50" cy="50" r="40" fill="red" onclick="alert('click')" onload="alert('load')" />
    +      </svg>
    +    `;
    +
    +    const sanitized = sanitizeSVGContent(maliciousSvg);
    +
    +    expect(sanitized).not.toContain('onclick');
    +    expect(sanitized).not.toContain('onload');
    +    expect(sanitized).toContain('<circle');
    +    expect(sanitized).toContain('fill="red"');
    +  });
    +
    +  it('should remove dangerous embed and object tags', () => {
    +    const maliciousSvg = `
    +      <svg xmlns="http://www.w3.org/2000/svg">
    +        <object data="malicious.swf"></object>
    +        <embed src="malicious.swf"></embed>
    +        <circle cx="50" cy="50" r="40" fill="red" />
    +      </svg>
    +    `;
    +
    +    const sanitized = sanitizeSVGContent(maliciousSvg);
    +
    +    // Note: DOMPurify with SVG profile may still allow some elements
    +    // The key security protection is removing script and event handlers
    +    expect(sanitized).toContain('<circle');
    +    expect(sanitized).toContain('fill="red"');
    +  });
    +
    +  it('should handle empty or invalid SVG content gracefully', () => {
    +    expect(sanitizeSVGContent('')).toBe('');
    +    expect(sanitizeSVGContent('<invalid>content</invalid>')).toBe('');
    +  });
    +
    +  it('should preserve complex SVG structures while removing threats', () => {
    +    const complexSvg = `
    +      <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
    +        <defs>
    +          <linearGradient id="grad1">
    +            <stop offset="0%" stop-color="red" />
    +            <stop offset="100%" stop-color="blue" />
    +          </linearGradient>
    +        </defs>
    +        <g transform="translate(50,50)">
    +          <script>malicious()</script>
    +          <circle cx="50" cy="50" r="40" fill="url(#grad1)" onclick="hack()" />
    +          <text x="50" y="60" text-anchor="middle" onload="evil()">Hello</text>
    +        </g>
    +      </svg>
    +    `;
    +
    +    const sanitized = sanitizeSVGContent(complexSvg);
    +
    +    // Should preserve safe elements and attributes
    +    expect(sanitized).toEqual(`
    +      <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
    +        <defs>
    +          <linearGradient id="grad1">
    +            <stop offset="0%" stop-color="red"></stop>
    +            <stop offset="100%" stop-color="blue"></stop>
    +          </linearGradient>
    +        </defs>
    +        <g transform="translate(50,50)">
    +          </g></svg>`);
    +  });
    +});
    
  • packages/utils/src/client/sanitize.ts+33 0 added
    @@ -0,0 +1,33 @@
    +import DOMPurify from 'dompurify';
    +
    +/**
    + * Sanitizes SVG content to prevent XSS attacks while preserving safe SVG elements and attributes
    + * @param content - The SVG content to sanitize
    + * @returns Sanitized SVG content safe for rendering
    + */
    +export const sanitizeSVGContent = (content: string): string => {
    +  return DOMPurify.sanitize(content, {
    +    FORBID_ATTR: [
    +      'onblur',
    +      'onchange',
    +      'onclick',
    +      'onerror',
    +      'onfocus',
    +      'onkeydown',
    +      'onkeypress',
    +      'onkeyup',
    +      'onload',
    +      'onmousedown',
    +      'onmouseout',
    +      'onmouseover',
    +      'onmouseup',
    +      'onreset',
    +      'onselect',
    +      'onsubmit',
    +      'onunload',
    +    ],
    +    FORBID_TAGS: ['embed', 'link', 'object', 'script', 'style'],
    +    KEEP_CONTENT: false,
    +    USE_PROFILES: { svg: true, svgFilters: true },
    +  });
    +};
    
  • src/features/Portal/Artifacts/Body/Renderer/SVG.tsx+7 3 modified
    @@ -1,15 +1,16 @@
    +import { copyImageToClipboard, sanitizeSVGContent } from '@lobechat/utils/client';
     import { Button, Dropdown, Tooltip } from '@lobehub/ui';
     import { App, Space } from 'antd';
     import { css, cx } from 'antd-style';
     import { CopyIcon, DownloadIcon } from 'lucide-react';
     import { domToPng } from 'modern-screenshot';
    +import { useMemo } from 'react';
     import { useTranslation } from 'react-i18next';
     import { Center, Flexbox } from 'react-layout-kit';
     
     import { BRANDING_NAME } from '@/const/branding';
     import { useChatStore } from '@/store/chat';
     import { chatPortalSelectors } from '@/store/chat/selectors';
    -import { copyImageToClipboard } from '@/utils/clipboard';
     
     const svgContainer = css`
       width: 100%;
    @@ -36,6 +37,9 @@ const SVGRenderer = ({ content }: SVGRendererProps) => {
       const { t } = useTranslation('portal');
       const { message } = App.useApp();
     
    +  // Sanitize SVG content to prevent XSS attacks
    +  const sanitizedContent = useMemo(() => sanitizeSVGContent(content), [content]);
    +
       const generatePng = async () => {
         return domToPng(document.querySelector(`#${DOM_ID}`) as HTMLDivElement, {
           features: {
    @@ -50,7 +54,7 @@ const SVGRenderer = ({ content }: SVGRendererProps) => {
         let dataUrl = '';
         if (type === 'png') dataUrl = await generatePng();
         else if (type === 'svg') {
    -      const blob = new Blob([content], { type: 'image/svg+xml' });
    +      const blob = new Blob([sanitizedContent], { type: 'image/svg+xml' });
     
           dataUrl = URL.createObjectURL(blob);
         }
    @@ -73,7 +77,7 @@ const SVGRenderer = ({ content }: SVGRendererProps) => {
         >
           <Center
             className={cx(svgContainer)}
    -        dangerouslySetInnerHTML={{ __html: content }}
    +        dangerouslySetInnerHTML={{ __html: sanitizedContent }}
             id={DOM_ID}
           />
           <Flexbox className={cx(actions)}>
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

9

News mentions

0

No linked articles in our index yet.