mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-02-12 17:08:14 +00:00
Compare commits
1 Commits
codex/nl2s
...
devin/1769
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff98f2a878 |
@@ -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 {
|
||||
<ul class="drawer-list">
|
||||
${grouped.map((group) => {
|
||||
if (group.items.length === 1) {
|
||||
return `<li><span class="drawer-code-link" data-node-id="${group.items[0]}">${group.items[0]}</span></li>`;
|
||||
return `<li><span class="drawer-code-link" data-node-id="${escapeHtml(group.items[0])}">${escapeHtml(group.items[0])}</span></li>`;
|
||||
} else {
|
||||
const groupId = group.items.join(',');
|
||||
const groupId = escapeHtml(group.items.join(','));
|
||||
return `
|
||||
<li>
|
||||
<div class="trigger-group" data-trigger-items="${groupId}" style="border-left: 2px solid var(--text-secondary); padding: 4px 0 4px 8px; margin: 2px 0; border-radius: 3px; cursor: pointer; transition: background 0.15s ease;">
|
||||
@@ -1458,7 +1489,7 @@ class DrawerManager {
|
||||
${group.items.length} routes <span style="opacity: 0.5; font-size: 9px;">→</span>
|
||||
</div>
|
||||
<div class="trigger-group-items" style="display: flex; flex-wrap: wrap; gap: 4px; pointer-events: none;">
|
||||
${group.items.map((t) => `<span class="drawer-code" style="opacity: 0.7;">${t}</span>`).join("")}
|
||||
${group.items.map((t) => `<span class="drawer-code" style="opacity: 0.7;">${escapeHtml(t)}</span>`).join("")}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
@@ -1535,17 +1566,17 @@ class DrawerManager {
|
||||
|
||||
renderConditionTree(condition, depth = 0) {
|
||||
if (typeof condition === "string") {
|
||||
return `<span class="drawer-code-link trigger-leaf" data-node-id="${condition}">${condition}</span>`;
|
||||
return `<span class="drawer-code-link trigger-leaf" data-node-id="${escapeHtml(condition)}">${escapeHtml(condition)}</span>`;
|
||||
}
|
||||
|
||||
if (condition.type === "AND" || condition.type === "OR") {
|
||||
const conditionType = condition.type;
|
||||
const conditionType = escapeHtml(condition.type);
|
||||
const color = conditionType === "AND" ? "{{ CREWAI_ORANGE }}" : "var(--text-secondary)";
|
||||
const bgColor = conditionType === "AND" ? "rgba(255,90,80,0.08)" : "rgba(102,102,102,0.06)";
|
||||
const hoverBg = conditionType === "AND" ? "rgba(255,90,80,0.15)" : "rgba(102,102,102,0.12)";
|
||||
|
||||
const triggerIds = this.extractTriggerIds(condition);
|
||||
const triggerIdsJson = JSON.stringify(triggerIds).replace(/"/g, '"');
|
||||
const triggerIdsJson = escapeHtml(JSON.stringify(triggerIds));
|
||||
|
||||
const stringChildren = condition.conditions.filter(c => typeof c === 'string');
|
||||
const nonStringChildren = condition.conditions.filter(c => typeof c !== 'string');
|
||||
@@ -1562,7 +1593,7 @@ class DrawerManager {
|
||||
if (group.items.length === 1) {
|
||||
return this.renderConditionTree(group.items[0], depth + 1);
|
||||
} else {
|
||||
const groupId = group.items.join(',');
|
||||
const groupId = escapeHtml(group.items.join(','));
|
||||
const groupColor = conditionType === "AND" ? "{{ CREWAI_ORANGE }}" : "var(--text-secondary)";
|
||||
const groupBgColor = conditionType === "AND" ? "rgba(255,90,80,0.08)" : "rgba(102,102,102,0.06)";
|
||||
const groupHoverBg = conditionType === "AND" ? "rgba(255,90,80,0.15)" : "rgba(102,102,102,0.12)";
|
||||
@@ -1572,7 +1603,7 @@ class DrawerManager {
|
||||
${group.items.length} routes <i data-lucide="chevron-down" style="width: 14px; height: 14px; color: ${groupColor};"></i>
|
||||
</div>
|
||||
<div class="trigger-group-items" style="display: flex; flex-wrap: wrap; gap: 4px; margin-top: 4px; pointer-events: none;">
|
||||
${group.items.map((t) => `<span class="drawer-code-link trigger-leaf" style="opacity: 0.7; cursor: default;">${t}</span>`).join("")}
|
||||
${group.items.map((t) => `<span class="drawer-code-link trigger-leaf" style="opacity: 0.7; cursor: default;">${escapeHtml(t)}</span>`).join("")}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -1615,7 +1646,7 @@ class DrawerManager {
|
||||
console.log('renderMetadata called with:', metadata);
|
||||
let metadataContent = "";
|
||||
|
||||
const nodeType = metadata.type || "unknown";
|
||||
const nodeType = escapeHtml(metadata.type || "unknown");
|
||||
const typeBadgeColor =
|
||||
nodeType === "start" || nodeType === "router"
|
||||
? "{{ CREWAI_ORANGE }}"
|
||||
@@ -1640,7 +1671,7 @@ class DrawerManager {
|
||||
metadataContent += `
|
||||
<div class="drawer-section">
|
||||
<div class="drawer-section-title">Condition</div>
|
||||
<span class="drawer-badge" style="background: ${bg}; color: ${color};">${metadata.condition_type}</span>
|
||||
<span class="drawer-badge" style="background: ${bg}; color: ${color};">${escapeHtml(metadata.condition_type)}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -1662,14 +1693,14 @@ class DrawerManager {
|
||||
|
||||
if (metadata.router_paths && metadata.router_paths.length > 0) {
|
||||
const uniqueRouterPaths = [...new Set(metadata.router_paths)];
|
||||
const routerPathsJson = JSON.stringify(uniqueRouterPaths).replace(/"/g, '"');
|
||||
const routerPathsJson = escapeHtml(JSON.stringify(uniqueRouterPaths));
|
||||
metadataContent += `
|
||||
<div class="drawer-section">
|
||||
<div class="drawer-section-title router-paths-title" data-router-paths="${routerPathsJson}" style="cursor: pointer; display: inline-flex; align-items: center; gap: 4px;">
|
||||
Router Paths <i data-lucide="chevron-down" style="width: 14px; height: 14px; color: var(--text-primary);"></i>
|
||||
</div>
|
||||
<ul class="drawer-list">
|
||||
${uniqueRouterPaths.map((p) => `<li><span class="drawer-code-link" data-node-id="${p}" style="color: {{ CREWAI_ORANGE }}; border-color: rgba(255,90,80,0.3);">${p}</span></li>`).join("")}
|
||||
${uniqueRouterPaths.map((p) => `<li><span class="drawer-code-link" data-node-id="${escapeHtml(p)}" style="color: {{ CREWAI_ORANGE }}; border-color: rgba(255,90,80,0.3);">${escapeHtml(p)}</span></li>`).join("")}
|
||||
</ul>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
<script src="https://unpkg.com/lucide@latest"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-python.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/dompurify/3.0.6/purify.min.js"></script>
|
||||
<script src="'{{ js_path }}'"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -667,4 +667,67 @@ def test_no_warning_for_properly_typed_router(caplog):
|
||||
# No warnings should be logged
|
||||
warning_messages = [r.message for r in caplog.records if r.levelno >= logging.WARNING]
|
||||
assert not any("Could not determine return paths" in msg for msg in warning_messages)
|
||||
assert not any("Found listeners waiting for triggers" in msg for msg in warning_messages)
|
||||
assert not any("Found listeners waiting for triggers" in msg for msg in warning_messages)
|
||||
|
||||
|
||||
def test_visualization_includes_dompurify_for_xss_protection():
|
||||
"""Test that visualization includes DOMPurify library for XSS protection."""
|
||||
flow = SimpleFlow()
|
||||
structure = build_flow_structure(flow)
|
||||
|
||||
html_file = visualize_flow_structure(structure, "test_flow.html", show=False)
|
||||
|
||||
with open(html_file, "r", encoding="utf-8") as f:
|
||||
html_content = f.read()
|
||||
|
||||
assert "dompurify" in html_content.lower()
|
||||
assert "cdnjs.cloudflare.com/ajax/libs/dompurify" in html_content
|
||||
|
||||
|
||||
def test_visualization_js_includes_sanitization_functions():
|
||||
"""Test that generated JS includes escapeHtml and sanitizeHtml functions."""
|
||||
flow = SimpleFlow()
|
||||
structure = build_flow_structure(flow)
|
||||
|
||||
html_file = visualize_flow_structure(structure, "test_flow.html", show=False)
|
||||
html_path = Path(html_file)
|
||||
|
||||
js_file = html_path.parent / f"{html_path.stem}_script.js"
|
||||
|
||||
js_content = js_file.read_text(encoding="utf-8")
|
||||
|
||||
assert "function escapeHtml" in js_content
|
||||
assert "function sanitizeHtml" in js_content
|
||||
assert "DOMPurify.sanitize" in js_content
|
||||
|
||||
|
||||
def test_visualization_uses_sanitize_html_for_drawer_content():
|
||||
"""Test that drawer content rendering uses sanitizeHtml."""
|
||||
flow = SimpleFlow()
|
||||
structure = build_flow_structure(flow)
|
||||
|
||||
html_file = visualize_flow_structure(structure, "test_flow.html", show=False)
|
||||
html_path = Path(html_file)
|
||||
|
||||
js_file = html_path.parent / f"{html_path.stem}_script.js"
|
||||
|
||||
js_content = js_file.read_text(encoding="utf-8")
|
||||
|
||||
assert "innerHTML = sanitizeHtml(content)" in js_content
|
||||
|
||||
|
||||
def test_visualization_escapes_user_controlled_values():
|
||||
"""Test that user-controlled values are escaped in render methods."""
|
||||
flow = SimpleFlow()
|
||||
structure = build_flow_structure(flow)
|
||||
|
||||
html_file = visualize_flow_structure(structure, "test_flow.html", show=False)
|
||||
html_path = Path(html_file)
|
||||
|
||||
js_file = html_path.parent / f"{html_path.stem}_script.js"
|
||||
|
||||
js_content = js_file.read_text(encoding="utf-8")
|
||||
|
||||
assert "escapeHtml(group.items[0])" in js_content
|
||||
assert "escapeHtml(condition)" in js_content
|
||||
assert "escapeHtml(metadata.type" in js_content
|
||||
|
||||
Reference in New Issue
Block a user