Skip to content
34 changes: 30 additions & 4 deletions actions/setup/js/sanitize_content.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -2416,11 +2416,37 @@ describe("sanitize_content.cjs", () => {
expect(mockCore.warning).not.toHaveBeenCalledWith(expect.stringContaining("Template-like syntax detected"));
});

it("should escape template delimiters in code blocks", () => {
// Template delimiters should still be escaped even in code blocks
// This is defense-in-depth - we escape everywhere
it("should preserve template delimiters inside inline code spans", () => {
// Template delimiters inside inline code spans must NOT be escaped –
// code content is reproduced verbatim.
const result = sanitizeContent("`code with {{ var }}`");
expect(result).toBe("`code with \\{\\{ var }}`");
expect(result).toBe("`code with {{ var }}`");
});

it("should preserve template delimiters inside fenced code blocks", () => {
// Template delimiters inside fenced code blocks must NOT be escaped.
const input = "Text before\n```\n{{ template_var }}\n```\nText after";
const result = sanitizeContent(input);
expect(result).toContain("{{ template_var }}");
expect(result).not.toContain("\\{\\{");
});

it("should preserve template delimiters inside GitHub suggestion blocks", () => {
// Suggestion blocks are fenced code blocks – their content is applied literally
// as a patch, so template delimiters must not be escaped.
const input = "Review comment\n```suggestion\nRefer to [Advanced {{fleet-server}} options](/ref.md).\n```";
const result = sanitizeContent(input);
expect(result).toContain("{{fleet-server}}");
expect(result).not.toContain("\\{\\{");
});

it("should still escape template delimiters outside code blocks", () => {
// Template delimiters in regular prose must still be escaped.
const input = "Outside: {{ var }}\n```\nInside: {{ safe }}\n```\nAlso outside: {{ other }}";
const result = sanitizeContent(input);
expect(result).toContain("\\{\\{ var }}");
expect(result).toContain("\\{\\{ other }}");
expect(result).toContain("{{ safe }}"); // inside fence – preserved
});

it("should handle real-world GitHub Actions template expressions", () => {
Expand Down
114 changes: 71 additions & 43 deletions actions/setup/js/sanitize_content_core.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -744,69 +744,97 @@ function neutralizeBotTriggers(s, maxBotMentions = MAX_BOT_TRIGGER_REFERENCES) {
* template syntax, but this prevents issues if content is later processed by
* template engines (Jinja2, Liquid, ERB, JavaScript template literals).
*
* Fenced code blocks (including GitHub suggestion blocks) and inline code spans are
* preserved verbatim so that legitimate source content inside code regions is not altered.
*
* @param {string} s - The string to process
* @returns {string} The string with escaped template delimiters
* @returns {string} The string with escaped template delimiters (outside code regions)
*/
function neutralizeTemplateDelimiters(s) {
if (!s || typeof s !== "string") {
return "";
}

let result = s;
let templatesDetected = false;
// Track which template types were detected (outside code regions) for deduped logging.
const detectedTypes = new Set();

// Escape Jinja2/Liquid double curly braces: {{ ... }}
// Replace {{ with \{\{ to prevent template evaluation
if (/\{\{/.test(result)) {
templatesDetected = true;
if (typeof core !== "undefined" && core.info) {
core.info("Template syntax detected: Jinja2/Liquid double braces {{");
/**
* Escapes template delimiters in a plain-text segment (no fenced blocks or inline code).
* @param {string} text - Plain text to escape
* @returns {string} Text with template delimiters escaped
*/
function escapeInText(text) {
let result = text;

// Escape Jinja2/Liquid double curly braces: {{ ... }}
// Replace {{ with \{\{ to prevent template evaluation
if (/\{\{/.test(result)) {
if (!detectedTypes.has("jinja2")) {
detectedTypes.add("jinja2");
if (typeof core !== "undefined" && core.info) {
core.info("Template syntax detected: Jinja2/Liquid double braces {{");
}
}
result = result.replace(/\{\{/g, "\\{\\{");
}
result = result.replace(/\{\{/g, "\\{\\{");
}

// Escape ERB delimiters: <%= ... %>
// Replace <%= with \<%= to prevent ERB evaluation
if (/<%=/.test(result)) {
templatesDetected = true;
if (typeof core !== "undefined" && core.info) {
core.info("Template syntax detected: ERB delimiter <%=");
// Escape ERB delimiters: <%= ... %>
Comment on lines +757 to +765

Copilot AI Apr 27, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

neutralizeTemplateDelimiters now calls core.info / core.warning unconditionally. This will throw a ReferenceError if core is not present in the global scope (e.g., when importing sanitize_content_core.cjs in a plain Node context without setupGlobals()/shim.cjs). Other functions in this file still guard core access via typeof core !== "undefined", so this introduces an inconsistency and a potential runtime regression when template patterns are present. Consider restoring the typeof core !== "undefined" && core.* guards here, or consistently referencing global.core with a safe fallback/no-op logger.

Copilot uses AI. Check for mistakes.
// Replace <%= with \<%= to prevent ERB evaluation
if (/<%=/.test(result)) {
if (!detectedTypes.has("erb")) {
detectedTypes.add("erb");
if (typeof core !== "undefined" && core.info) {
core.info("Template syntax detected: ERB delimiter <%=");
}
}
result = result.replace(/<%=/g, "\\<%=");
}
result = result.replace(/<%=/g, "\\<%=");
}

// Escape JavaScript template literal delimiters: ${ ... }
// Replace ${ with \$\{ to prevent template literal evaluation
if (/\$\{/.test(result)) {
templatesDetected = true;
if (typeof core !== "undefined" && core.info) {
core.info("Template syntax detected: JavaScript template literal ${");
// Escape JavaScript template literal delimiters: ${ ... }
// Replace ${ with \$\{ to prevent template literal evaluation
if (/\$\{/.test(result)) {
if (!detectedTypes.has("js")) {
detectedTypes.add("js");
if (typeof core !== "undefined" && core.info) {
core.info("Template syntax detected: JavaScript template literal ${");
}
}
result = result.replace(/\$\{/g, "\\$\\{");
}
result = result.replace(/\$\{/g, "\\$\\{");
}

// Escape Jinja2 comment delimiters: {# ... #}
// Replace {# with \{\# to prevent Jinja2 comment evaluation
if (/\{#/.test(result)) {
templatesDetected = true;
if (typeof core !== "undefined" && core.info) {
core.info("Template syntax detected: Jinja2 comment {#");
// Escape Jinja2 comment delimiters: {# ... #}
// Replace {# with \{\# to prevent Jinja2 comment evaluation
if (/\{#/.test(result)) {
if (!detectedTypes.has("jinja2comment")) {
detectedTypes.add("jinja2comment");
if (typeof core !== "undefined" && core.info) {
core.info("Template syntax detected: Jinja2 comment {#");
}
}
result = result.replace(/\{#/g, "\\{\\#");
}
result = result.replace(/\{#/g, "\\{\\#");
}

// Escape Jekyll raw blocks: {% raw %} and {% endraw %}
// Replace {% with \{\% to prevent Jekyll directive evaluation
if (/\{%/.test(result)) {
templatesDetected = true;
if (typeof core !== "undefined" && core.info) {
core.info("Template syntax detected: Jekyll/Liquid directive {%");
// Escape Jekyll raw blocks: {% raw %} and {% endraw %}
// Replace {% with \{\% to prevent Jekyll directive evaluation
if (/\{%/.test(result)) {
if (!detectedTypes.has("jekyll")) {
detectedTypes.add("jekyll");
if (typeof core !== "undefined" && core.info) {
core.info("Template syntax detected: Jekyll/Liquid directive {%");
}
}
result = result.replace(/\{%/g, "\\{\\%");
}
result = result.replace(/\{%/g, "\\{\\%");

return result;
}

// Apply escaping only to non-code regions (skip fenced code blocks and inline code spans).
// This preserves the verbatim content of suggestion blocks and other code fences.
const result = applyToNonCodeRegions(s, escapeInText);

// Log a summary warning if any template patterns were detected
if (templatesDetected && typeof core !== "undefined" && core.warning) {
if (detectedTypes.size > 0 && typeof core !== "undefined" && core.warning) {
core.warning(
"Template-like syntax detected and escaped. " +
"This is a defense-in-depth measure to prevent potential template injection " +
Expand Down
Loading