fix(webui): sanitize chat markdown rendering

This commit is contained in:
Muhammad Ali 2026-05-12 16:40:48 -04:00
parent 7ba1d61e34
commit 35cfcb3be3
2 changed files with 29 additions and 6 deletions

View file

@ -14,6 +14,7 @@ import { formatDuration } from "./time-utils.js";
import { Scroller } from "./scroller.js";
import { callJsExtensions } from "/js/extensions.js";
import { addBlankTargetsToLinks } from "/js/html-links.js";
import { sanitizeHtml } from "/js/safe-markdown.js";
// Delay before collapsing previous steps when a new step is added
const STEP_COLLAPSE_DELAY = {
@ -708,6 +709,10 @@ export function _drawMessage({
processedContent = convertImgFilePaths(processedContent);
processedContent = convertFilePaths(processedContent);
processedContent = marked.parse(processedContent, { breaks: true });
processedContent = sanitizeHtml(processedContent, {
allowDataImages: true,
allowLatex: latex,
});
processedContent = convertPathsToLinks(processedContent);
processedContent = addBlankTargetsToLinks(processedContent);

View file

@ -29,6 +29,17 @@ const DOMPURIFY_CONFIG = Object.freeze({
FORBID_TAGS: ["script", "iframe", "object", "embed", "svg", "math"],
});
const DATA_IMAGE_URL_PATTERN =
/^data:image\/(?:png|jpe?g|gif|webp|bmp);base64,[a-z0-9+/=\s]+$/i;
function getDompurifyConfig(options = {}) {
const config = { ...DOMPURIFY_CONFIG };
if (options.allowLatex) {
config.ADD_TAGS = ["latex"];
}
return config;
}
function parseGithubRepoContext(githubUrl) {
if (!githubUrl || typeof githubUrl !== "string") return null;
@ -77,9 +88,16 @@ function isGithubRepoRoutePath(repoPath) {
return GITHUB_REPO_ROUTE_PREFIXES.has(firstSegment);
}
function isSafeUrlValue(value, attributeName) {
function isSafeUrlValue(value, attributeName, options = {}) {
const normalized = String(value || "").trim();
if (!normalized) return true;
if (
options.allowDataImages &&
attributeName === "src" &&
DATA_IMAGE_URL_PATTERN.test(normalized)
) {
return true;
}
if (
normalized.startsWith("#") ||
normalized.startsWith("/") ||
@ -108,14 +126,14 @@ function isSafeUrlValue(value, attributeName) {
return false;
}
function stripUnsafeUrlAttributes(html) {
function stripUnsafeUrlAttributes(html, options = {}) {
const doc = new DOMParser().parseFromString(html, "text/html");
doc.querySelectorAll("[href], [src]").forEach((element) => {
for (const attributeName of ["href", "src"]) {
if (!element.hasAttribute(attributeName)) continue;
const value = element.getAttribute(attributeName) || "";
if (!isSafeUrlValue(value, attributeName)) {
if (!isSafeUrlValue(value, attributeName, options)) {
element.removeAttribute(attributeName);
}
}
@ -124,10 +142,10 @@ function stripUnsafeUrlAttributes(html) {
return doc.body.innerHTML;
}
export function sanitizeHtml(html) {
export function sanitizeHtml(html, options = {}) {
if (!html || typeof html !== "string") return "";
const sanitized = DOMPurify.sanitize(html, DOMPURIFY_CONFIG);
return stripUnsafeUrlAttributes(sanitized);
const sanitized = DOMPurify.sanitize(html, getDompurifyConfig(options));
return stripUnsafeUrlAttributes(sanitized, options);
}
export function rebaseGithubReadmeHtml(html, githubUrl, branch) {