mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
fix(ui): catch marked.js parse errors to prevent Control UI crash (#36445)
- Prevent Control UI session render crashes when `marked.parse()` encounters pathological recursive markdown by safely falling back to escaped `<pre>` output. - Tighten markdown fallback regression coverage and keep changelog attribution in sync for this crash-hardening path. Co-authored-by: Bin Deng <dengbin@romangic.com> Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { marked } from "marked";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { toSanitizedMarkdownHtml } from "./markdown.ts";
|
||||
|
||||
describe("toSanitizedMarkdownHtml", () => {
|
||||
@@ -82,4 +83,36 @@ describe("toSanitizedMarkdownHtml", () => {
|
||||
// Pipes from table delimiters must not appear as raw text
|
||||
expect(html).not.toContain("|------|");
|
||||
});
|
||||
|
||||
it("does not throw on deeply nested emphasis markers (#36213)", () => {
|
||||
// Pathological patterns that can trigger catastrophic backtracking / recursion
|
||||
const nested = "*".repeat(500) + "text" + "*".repeat(500);
|
||||
expect(() => toSanitizedMarkdownHtml(nested)).not.toThrow();
|
||||
const html = toSanitizedMarkdownHtml(nested);
|
||||
expect(html).toContain("text");
|
||||
});
|
||||
|
||||
it("does not throw on deeply nested brackets (#36213)", () => {
|
||||
const nested = "[".repeat(200) + "link" + "]".repeat(200) + "(" + "x".repeat(200) + ")";
|
||||
expect(() => toSanitizedMarkdownHtml(nested)).not.toThrow();
|
||||
const html = toSanitizedMarkdownHtml(nested);
|
||||
expect(html).toContain("link");
|
||||
});
|
||||
|
||||
it("falls back to escaped plain text if marked.parse throws (#36213)", () => {
|
||||
const parseSpy = vi.spyOn(marked, "parse").mockImplementation(() => {
|
||||
throw new Error("forced parse failure");
|
||||
});
|
||||
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
const input = `Fallback **probe** ${Date.now()}`;
|
||||
try {
|
||||
const html = toSanitizedMarkdownHtml(input);
|
||||
expect(html).toContain('<pre class="code-block">');
|
||||
expect(html).toContain("Fallback **probe**");
|
||||
expect(warnSpy).toHaveBeenCalledOnce();
|
||||
} finally {
|
||||
parseSpy.mockRestore();
|
||||
warnSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -110,11 +110,20 @@ export function toSanitizedMarkdownHtml(markdown: string): string {
|
||||
}
|
||||
return sanitized;
|
||||
}
|
||||
const rendered = marked.parse(`${truncated.text}${suffix}`, {
|
||||
renderer: htmlEscapeRenderer,
|
||||
gfm: true,
|
||||
breaks: true,
|
||||
}) as string;
|
||||
let rendered: string;
|
||||
try {
|
||||
rendered = marked.parse(`${truncated.text}${suffix}`, {
|
||||
renderer: htmlEscapeRenderer,
|
||||
gfm: true,
|
||||
breaks: true,
|
||||
}) as string;
|
||||
} catch (err) {
|
||||
// Fall back to escaped plain text when marked.parse() throws (e.g.
|
||||
// infinite recursion on pathological markdown patterns — #36213).
|
||||
console.warn("[markdown] marked.parse failed, falling back to plain text:", err);
|
||||
const escaped = escapeHtml(`${truncated.text}${suffix}`);
|
||||
rendered = `<pre class="code-block">${escaped}</pre>`;
|
||||
}
|
||||
const sanitized = DOMPurify.sanitize(rendered, sanitizeOptions);
|
||||
if (input.length <= MARKDOWN_CACHE_MAX_CHARS) {
|
||||
setCachedMarkdown(input, sanitized);
|
||||
|
||||
Reference in New Issue
Block a user