From ff98f2a878bd624d2fac5052c70091b80085b186 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sun, 1 Feb 2026 13:10:35 +0000 Subject: [PATCH] fix: Add XSS protection to flow visualization (issue #4326) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add DOMPurify CDN to HTML template for sanitizing HTML content - Add escapeHtml helper function to escape user-controlled text - Add sanitizeHtml function using DOMPurify with allowed tags/attributes - Apply sanitization to drawer content before setting innerHTML - Escape user-controlled values in renderTriggerCondition, renderConditionTree, and renderMetadata methods - Add tests to verify XSS protection is in place Co-Authored-By: João --- .../flow/visualization/assets/interactive.js | 57 ++++++++++++---- .../assets/interactive_flow.html.j2 | 1 + lib/crewai/tests/test_flow_visualization.py | 65 ++++++++++++++++++- 3 files changed, 109 insertions(+), 14 deletions(-) diff --git a/lib/crewai/src/crewai/flow/visualization/assets/interactive.js b/lib/crewai/src/crewai/flow/visualization/assets/interactive.js index 10788727f..fb7658619 100644 --- a/lib/crewai/src/crewai/flow/visualization/assets/interactive.js +++ b/lib/crewai/src/crewai/flow/visualization/assets/interactive.js @@ -64,6 +64,37 @@ function highlightPython(code) { return Prism.highlight(code, Prism.languages.python, "python"); } +function escapeHtml(text) { + if (text === null || text === undefined) { + return ""; + } + const str = String(text); + const div = document.createElement("div"); + div.textContent = str; + return div.innerHTML; +} + +function sanitizeHtml(html) { + if (typeof DOMPurify !== "undefined") { + return DOMPurify.sanitize(html, { + ALLOWED_TAGS: [ + "div", "span", "pre", "code", "ul", "li", "button", "svg", "line", + "rect", "path", "polyline", "i", "label", "select", "option" + ], + ALLOWED_ATTR: [ + "class", "style", "id", "data-code", "data-node-id", "data-trigger-items", + "data-trigger-group", "data-router-paths", "data-condition-label", + "data-lucide", "viewBox", "fill", "stroke", "stroke-width", + "stroke-dasharray", "stroke-linecap", "stroke-linejoin", + "x", "y", "x1", "y1", "x2", "y2", "width", "height", "rx", + "points", "d", "onmouseover", "onmouseout" + ], + ALLOW_DATA_ATTR: true, + }); + } + return html; +} + class NodeRenderer { constructor(nodes, networkManager) { this.nodes = nodes; @@ -1428,7 +1459,7 @@ class DrawerManager { content += this.renderSourceCode(metadata); } - this.elements.content.innerHTML = content; + this.elements.content.innerHTML = sanitizeHtml(content); this.attachContentEventListeners(nodeName); // Initialize Lucide icons in the newly rendered drawer content @@ -1448,9 +1479,9 @@ class DrawerManager {