mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-23 10:42:00 +00:00
fix(security): sanitize kanban markdown html
This commit is contained in:
parent
8e4d2fd23f
commit
5b45fb269a
2 changed files with 56 additions and 1 deletions
44
plugins/kanban/dashboard/dist/index.js
vendored
44
plugins/kanban/dashboard/dist/index.js
vendored
|
|
@ -334,6 +334,48 @@
|
|||
);
|
||||
return html;
|
||||
}
|
||||
const MARKDOWN_ALLOWED_TAGS = new Set([
|
||||
"a",
|
||||
"code",
|
||||
"em",
|
||||
"h1",
|
||||
"h2",
|
||||
"h3",
|
||||
"h4",
|
||||
"li",
|
||||
"p",
|
||||
"pre",
|
||||
"strong",
|
||||
"ul",
|
||||
]);
|
||||
function escapeAttribute(value) {
|
||||
return escapeHtml(value).replace(/`/g, "`");
|
||||
}
|
||||
function sanitizeMarkdownAttrs(tag, attrs) {
|
||||
if (tag === "a") {
|
||||
const hrefMatch =
|
||||
/\shref=(["'])(.*?)\1/i.exec(attrs) ||
|
||||
/\shref=([^\s>]+)/i.exec(attrs);
|
||||
const href = hrefMatch ? (hrefMatch[2] || hrefMatch[1] || "").trim() : "";
|
||||
if (!/^(https?:\/\/|mailto:)/i.test(href)) return "";
|
||||
return ` href="${escapeAttribute(href)}" target="_blank" rel="noopener noreferrer"`;
|
||||
}
|
||||
if (tag === "pre" && /\sclass=(["'])hermes-kanban-md-code\1/i.test(attrs)) {
|
||||
return ' class="hermes-kanban-md-code"';
|
||||
}
|
||||
return "";
|
||||
}
|
||||
function sanitizeMarkdownHtml(html) {
|
||||
return String(html || "").replace(
|
||||
/<\/?([a-zA-Z][A-Za-z0-9-]*)([^>]*)>/g,
|
||||
(match, rawTag, attrs) => {
|
||||
const tag = rawTag.toLowerCase();
|
||||
if (!MARKDOWN_ALLOWED_TAGS.has(tag)) return "";
|
||||
if (/^<\s*\//.test(match)) return `</${tag}>`;
|
||||
return `<${tag}${sanitizeMarkdownAttrs(tag, attrs || "")}>`;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function MarkdownBlock(props) {
|
||||
const enabled = props.enabled !== false;
|
||||
|
|
@ -342,7 +384,7 @@
|
|||
}
|
||||
return h("div", {
|
||||
className: "hermes-kanban-md",
|
||||
dangerouslySetInnerHTML: { __html: renderMarkdown(props.source || "") },
|
||||
dangerouslySetInnerHTML: { __html: sanitizeMarkdownHtml(renderMarkdown(props.source || "")) },
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -247,6 +247,19 @@ def test_dashboard_initial_board_uses_backend_current_when_unpinned():
|
|||
assert 'readSelectedBoard() || "default"' not in js
|
||||
|
||||
|
||||
def test_dashboard_markdown_html_is_sanitized_before_render():
|
||||
"""Markdown rendering must sanitize HTML before dangerouslySetInnerHTML."""
|
||||
|
||||
repo_root = Path(__file__).resolve().parents[2]
|
||||
bundle = repo_root / "plugins" / "kanban" / "dashboard" / "dist" / "index.js"
|
||||
js = bundle.read_text()
|
||||
|
||||
assert "function sanitizeMarkdownHtml(html)" in js
|
||||
assert "MARKDOWN_ALLOWED_TAGS" in js
|
||||
assert "sanitizeMarkdownHtml(renderMarkdown(props.source || \"\"))" in js
|
||||
assert "dangerouslySetInnerHTML: { __html: renderMarkdown(props.source || \"\") }" not in js
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# GET /tasks/:id returns body + comments + events + links
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue