diff --git a/.github/smoke-test-22395402042.json b/.github/smoke-test-22395402042.json
index 8f941793eb..eb8cab0afc 100644
--- a/.github/smoke-test-22395402042.json
+++ b/.github/smoke-test-22395402042.json
@@ -1 +1 @@
-{"smoke_test": "run-22395402042", "note": "Test file for PR push validation"}
+{ "smoke_test": "run-22395402042", "note": "Test file for PR push validation" }
diff --git a/actions/setup/js/sanitize_content.test.cjs b/actions/setup/js/sanitize_content.test.cjs
index 8c38358862..174b6ecc0b 100644
--- a/actions/setup/js/sanitize_content.test.cjs
+++ b/actions/setup/js/sanitize_content.test.cjs
@@ -482,6 +482,26 @@ describe("sanitize_content.cjs", () => {
const result = sanitizeContent(input);
expect(result).toBe(input);
});
+
+ it("should preserve custom XML tags that are not HTML elements", () => {
+ const input = "false";
+ const result = sanitizeContent(input);
+ expect(result).toBe(input);
+ });
+
+ it("should preserve MSBuild XML tags in markdown code blocks", () => {
+ const input = "```xml\nfalse\n```";
+ const result = sanitizeContent(input);
+ expect(result).toBe(input);
+ });
+
+ it("should still convert known HTML elements like div and script", () => {
+ const result1 = sanitizeContent("
content
");
+ expect(result1).toBe("(div)content(/div)");
+
+ const result2 = sanitizeContent("");
+ expect(result2).toBe("(script)alert('xss')(/script)");
+ });
});
describe("ANSI escape sequence removal", () => {
diff --git a/actions/setup/js/sanitize_content_core.cjs b/actions/setup/js/sanitize_content_core.cjs
index ea83b2b807..cd4967cf9b 100644
--- a/actions/setup/js/sanitize_content_core.cjs
+++ b/actions/setup/js/sanitize_content_core.cjs
@@ -380,6 +380,99 @@ function convertXmlTags(s) {
"ul",
];
+ // Known HTML5 elements - only these will be converted to prevent HTML injection.
+ // Custom XML elements (e.g., MSBuild, SVG custom, proprietary) are left unchanged
+ // since they cannot be rendered as HTML by GitHub's markdown renderer.
+ const knownHtmlElements = new Set([
+ ...allowedTags,
+ "a",
+ "address",
+ "area",
+ "article",
+ "aside",
+ "audio",
+ "base",
+ "bdi",
+ "bdo",
+ "body",
+ "button",
+ "canvas",
+ "caption",
+ "cite",
+ "col",
+ "colgroup",
+ "data",
+ "datalist",
+ "dd",
+ "dfn",
+ "dialog",
+ "div",
+ "dl",
+ "dt",
+ "embed",
+ "fieldset",
+ "figcaption",
+ "figure",
+ "footer",
+ "form",
+ "head",
+ "header",
+ "hgroup",
+ "html",
+ "iframe",
+ "img",
+ "input",
+ "label",
+ "legend",
+ "link",
+ "main",
+ "map",
+ "menu",
+ "meta",
+ "meter",
+ "nav",
+ "noscript",
+ "object",
+ "optgroup",
+ "option",
+ "output",
+ "picture",
+ "progress",
+ "q",
+ "rp",
+ "rt",
+ "ruby",
+ "samp",
+ "script",
+ "section",
+ "select",
+ "slot",
+ "small",
+ "source",
+ "style",
+ "template",
+ "textarea",
+ "tfoot",
+ "time",
+ "title",
+ "track",
+ "u",
+ "var",
+ "video",
+ "wbr",
+ ]);
+
+ // SVG elements that can be embedded in HTML5 and exploited via event handlers.
+ // Each element name is derived from the SVG specification.
+ for (const svgEl of "animate animateMotion animateTransform circle clipPath defs desc ellipse feBlend feColorMatrix feComponentTransfer feComposite feConvolveMatrix feDiffuseLighting feDisplacementMap feDropShadow feFlood feFuncA feFuncB feFuncG feFuncR feGaussianBlur feImage feMerge feMergeNode feMorphology feOffset fePointLight feSpecularLighting feSpotLight feTile feTurbulence filter foreignObject g image line linearGradient marker mask metadata mpath path pattern polygon polyline radialGradient rect set stop svg switch symbol text textPath tspan use view".split(
+ " "
+ )) {
+ knownHtmlElements.add(svgEl);
+ knownHtmlElements.add(svgEl.toLowerCase());
+ }
+ // MathML root element that can be embedded in HTML5.
+ knownHtmlElements.add("math");
+
// First, process CDATA sections specially - convert tags inside them and the CDATA markers
s = s.replace(//g, (match, content) => {
// Convert tags inside CDATA content
@@ -391,7 +484,7 @@ function convertXmlTags(s) {
// Convert opening tags: or to (tag) or (tag attr="value")
// Convert closing tags: to (/tag)
// Convert self-closing tags: or to (tag/) or (tag /)
- // But preserve allowed safe tags
+ // But preserve allowed safe tags and unknown (non-HTML) custom XML tags
return s.replace(/<(\/?[A-Za-z!][^>]*?)>/g, (match, tagContent) => {
// Extract tag name from the content (handle closing tags and attributes)
const tagNameMatch = tagContent.match(/^\/?\s*([A-Za-z][A-Za-z0-9]*)/);
@@ -400,8 +493,11 @@ function convertXmlTags(s) {
if (allowedTags.includes(tagName)) {
return match; // Preserve allowed tags
}
+ if (!knownHtmlElements.has(tagName)) {
+ return match; // Preserve non-HTML custom XML tags (e.g., MSBuild, custom namespaces)
+ }
}
- return `(${tagContent})`; // Convert other tags to parentheses
+ return `(${tagContent})`; // Convert known HTML tags to parentheses
});
}