mirror of
https://github.com/zadam/trilium.git
synced 2026-05-08 23:16:42 +02:00
fix(llm): XSS risk when displaying the message
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import type { LlmMessage, LlmCitation, LlmChatConfig, LlmUsage, LlmModelInfo } from "@triliumnext/commons";
|
||||
import type { LlmChatConfig, LlmCitation, LlmMessage, LlmModelInfo,LlmUsage } from "@triliumnext/commons";
|
||||
|
||||
import server from "./server.js";
|
||||
|
||||
/**
|
||||
@@ -98,7 +99,7 @@ export async function streamChatCompletion(
|
||||
break;
|
||||
}
|
||||
} catch {
|
||||
// Ignore JSON parse errors for partial data
|
||||
console.error("Failed to parse SSE data line:", line, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import DOMPurify from "dompurify";
|
||||
import type { CSSProperties, HTMLProps, RefObject } from "preact/compat";
|
||||
|
||||
type HTMLElementLike = string | HTMLElement | JQuery<HTMLElement>;
|
||||
@@ -14,16 +15,16 @@ export default function RawHtml({containerRef, ...props}: RawHtmlProps & { conta
|
||||
}
|
||||
|
||||
export function RawHtmlBlock({containerRef, ...props}: RawHtmlProps & { containerRef?: RefObject<HTMLDivElement>}) {
|
||||
return <div ref={containerRef} {...getProps(props)} />
|
||||
return <div ref={containerRef} {...getProps(props)} />;
|
||||
}
|
||||
|
||||
function getProps({ className, html, style, onClick }: RawHtmlProps) {
|
||||
return {
|
||||
className: className,
|
||||
className,
|
||||
dangerouslySetInnerHTML: getHtml(html ?? ""),
|
||||
style,
|
||||
onClick
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function getHtml(html: string | HTMLElement | JQuery<HTMLElement>) {
|
||||
@@ -39,3 +40,19 @@ export function getHtml(html: string | HTMLElement | JQuery<HTMLElement>) {
|
||||
__html: html as string
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders HTML content sanitized via DOMPurify to prevent XSS.
|
||||
* Use this instead of {@link RawHtml} when the HTML originates from
|
||||
* untrusted sources (e.g. LLM responses, user-generated markdown).
|
||||
*/
|
||||
export function SanitizedHtml({ className, html, style }: { className?: string; html: string; style?: CSSProperties }) {
|
||||
return (
|
||||
<div
|
||||
className={className}
|
||||
style={style}
|
||||
// eslint-disable-next-line react/no-danger
|
||||
dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(html) }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import "./LlmChat.css";
|
||||
|
||||
import DOMPurify from "dompurify";
|
||||
import { marked } from "marked";
|
||||
import { useMemo } from "preact/hooks";
|
||||
|
||||
import { t } from "../../../services/i18n.js";
|
||||
import utils from "../../../services/utils.js";
|
||||
import { SanitizedHtml } from "../../react/RawHtml.js";
|
||||
import { type ContentBlock, getMessageText, type StoredMessage, type ToolCall } from "./llm_chat_types.js";
|
||||
|
||||
function shortenNumber(n: number): string {
|
||||
@@ -20,10 +20,9 @@ marked.setOptions({
|
||||
gfm: true // GitHub Flavored Markdown
|
||||
});
|
||||
|
||||
/** Parse markdown and sanitize the resulting HTML to prevent XSS. */
|
||||
/** Parse markdown to HTML. Sanitization is handled by SanitizedHtml. */
|
||||
function renderMarkdown(markdown: string): string {
|
||||
const raw = marked.parse(markdown) as string;
|
||||
return DOMPurify.sanitize(raw);
|
||||
return marked.parse(markdown) as string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
@@ -75,10 +74,7 @@ function renderContentBlocks(blocks: ContentBlock[], isStreaming?: boolean) {
|
||||
const html = renderMarkdown(block.content);
|
||||
return (
|
||||
<div key={idx}>
|
||||
<div
|
||||
className="llm-chat-markdown"
|
||||
dangerouslySetInnerHTML={{ __html: html }}
|
||||
/>
|
||||
<SanitizedHtml className="llm-chat-markdown" html={html} />
|
||||
{isStreaming && idx === blocks.length - 1 && <span className="llm-chat-cursor" />}
|
||||
</div>
|
||||
);
|
||||
@@ -143,10 +139,7 @@ export default function ChatMessage({ message, isStreaming }: Props) {
|
||||
renderContentBlocks(message.content as ContentBlock[], isStreaming)
|
||||
) : (
|
||||
<>
|
||||
<div
|
||||
className="llm-chat-markdown"
|
||||
dangerouslySetInnerHTML={{ __html: renderedContent || "" }}
|
||||
/>
|
||||
<SanitizedHtml className="llm-chat-markdown" html={renderedContent || ""} />
|
||||
{isStreaming && <span className="llm-chat-cursor" />}
|
||||
</>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user