From c294c26b2b74f908732c55005bf079b04c76c470 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 23 Apr 2026 11:55:05 +0000 Subject: [PATCH 1/2] Initial plan From 74a8fc0911574a36871ac5ac9584dd2653426d9f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 23 Apr 2026 12:04:21 +0000 Subject: [PATCH 2/2] fix: add neutralizeMarkdownLinkTitles and neutralizeTemplateDelimiters to allowedAliases branch in sanitize_content.cjs (XPIA fix) The allowedAliases branch in sanitize_content.cjs was missing calls to neutralizeMarkdownLinkTitles and neutralizeTemplateDelimiters that are present in sanitizeContentCore. This created an XPIA channel where AI agents could embed hidden payloads in markdown link titles that would survive output sanitization when add_comment.cjs called sanitizeContent with allowedAliases set. - Import neutralizeMarkdownLinkTitles and neutralizeTemplateDelimiters - Apply neutralizeMarkdownLinkTitles after removeXmlComments and before neutralizeMentions (matching position in sanitizeContentCore) - Apply neutralizeTemplateDelimiters after neutralizeBotTriggers and before balanceCodeRegions (matching position in sanitizeContentCore) - Add regression tests for both fixes in the allowedAliases branch Agent-Logs-Url: https://github.com/github/gh-aw/sessions/2be1b8af-00b0-4c58-a6f9-2ac927186947 Co-authored-by: szabta89 <1330202+szabta89@users.noreply.github.com> --- actions/setup/js/sanitize_content.cjs | 12 ++++ actions/setup/js/sanitize_content.test.cjs | 71 ++++++++++++++++++++++ 2 files changed, 83 insertions(+) diff --git a/actions/setup/js/sanitize_content.cjs b/actions/setup/js/sanitize_content.cjs index 8218acdcdff..71489a33684 100644 --- a/actions/setup/js/sanitize_content.cjs +++ b/actions/setup/js/sanitize_content.cjs @@ -18,9 +18,11 @@ const { neutralizeCommands, neutralizeGitHubReferences, removeXmlComments, + neutralizeMarkdownLinkTitles, convertXmlTags, applyToNonCodeRegions, neutralizeBotTriggers, + neutralizeTemplateDelimiters, applyTruncation, hardenUnicodeText, } = require("./sanitize_content_core.cjs"); @@ -94,6 +96,12 @@ function sanitizeContent(content, maxLengthOrOptions) { // preventing the full pattern from being matched. sanitized = applyToNonCodeRegions(sanitized, removeXmlComments); + // Remove markdown link titles — a steganographic injection channel analogous to HTML comments. + // Quoted title text ([text](url "TITLE") and [ref]: url "TITLE") is invisible in GitHub's + // rendered markdown (shown only as hover-tooltips) but reaches the AI model verbatim. + // Must run before mention neutralization for the same ordering reason as removeXmlComments. + sanitized = applyToNonCodeRegions(sanitized, neutralizeMarkdownLinkTitles); + // Neutralize @mentions with selective filtering (custom logic for allowed aliases) sanitized = neutralizeMentions(sanitized, allowedAliasesLowercase); @@ -113,6 +121,10 @@ function sanitizeContent(content, maxLengthOrOptions) { // Neutralize bot triggers sanitized = neutralizeBotTriggers(sanitized, maxBotMentions); + // Neutralize template syntax delimiters (defense-in-depth) + // This prevents potential issues if content is processed by downstream template engines + sanitized = neutralizeTemplateDelimiters(sanitized); + // Balance markdown code regions to fix improperly nested fences // This repairs markdown where AI models generate nested code blocks at the same indentation sanitized = balanceCodeRegions(sanitized); diff --git a/actions/setup/js/sanitize_content.test.cjs b/actions/setup/js/sanitize_content.test.cjs index 15ae3b886ec..2f438acf1f3 100644 --- a/actions/setup/js/sanitize_content.test.cjs +++ b/actions/setup/js/sanitize_content.test.cjs @@ -2325,4 +2325,75 @@ describe("sanitize_content.cjs", () => { expect(result).toContain("(example.com/redacted)"); // URL redacted (not in allowed domains) }); }); + + describe("allowedAliases branch: markdown link title neutralization (XPIA regression)", () => { + it("should strip hidden double-quoted inline link title when allowedAliases is set", () => { + // Regression: allowedAliases branch previously skipped neutralizeMarkdownLinkTitles, + // allowing XPIA payloads to survive in hover-tooltip text. + const result = sanitizeContent('[text](https://github.com "SYSTEM: malicious payload")', { + allowedAliases: ["user"], + }); + expect(result).toBe("[text (SYSTEM: malicious payload)](https://github.com)"); + }); + + it("should strip hidden single-quoted inline link title when allowedAliases is set", () => { + const result = sanitizeContent("[text](https://github.com 'injected payload')", { + allowedAliases: ["user"], + }); + expect(result).toBe("[text (injected payload)](https://github.com)"); + }); + + it("should strip hidden parenthesized inline link title when allowedAliases is set", () => { + const result = sanitizeContent("[text](https://github.com (injected payload))", { + allowedAliases: ["user"], + }); + expect(result).toBe("[text (injected payload)](https://github.com)"); + }); + + it("should strip title from reference-style link definition when allowedAliases is set", () => { + const result = sanitizeContent('[x][ref]\n\n[ref]: https://github.com "XPIA payload"', { + allowedAliases: ["user"], + }); + expect(result).toBe("[x][ref]\n\n[ref]: https://github.com"); + }); + + it("should neutralize link title with @mention payload when allowedAliases is set", () => { + // The title moves to visible link text where the non-allowed @mention is then neutralized + const result = sanitizeContent('[text](https://github.com "@attacker inject payload")', { + allowedAliases: ["author"], + }); + expect(result).toBe("[text (`@attacker` inject payload)](https://github.com)"); + }); + + it("should preserve links without titles unchanged when allowedAliases is set", () => { + const result = sanitizeContent("[safe link](https://github.com)", { + allowedAliases: ["user"], + }); + expect(result).toBe("[safe link](https://github.com)"); + }); + }); + + describe("allowedAliases branch: template delimiter neutralization (XPIA regression)", () => { + it("should neutralize Jinja2/Liquid double braces when allowedAliases is set", () => { + // Regression: allowedAliases branch previously skipped neutralizeTemplateDelimiters + const result = sanitizeContent("Result: {{ secret.token }}", { allowedAliases: ["user"] }); + expect(result).toContain("\\{\\{"); + }); + + it("should neutralize Liquid block tags when allowedAliases is set", () => { + const result = sanitizeContent("{% if condition %}value{% endif %}", { allowedAliases: ["user"] }); + expect(result).toContain("\\{\\%"); + }); + + it("should neutralize ERB tags when allowedAliases is set", () => { + const result = sanitizeContent("<%= secret %>", { allowedAliases: ["user"] }); + expect(result).toContain("\\<%="); + }); + + it("should neutralize template delimiters while preserving allowed @mention", () => { + const result = sanitizeContent("@author: {{ secret }}", { allowedAliases: ["author"] }); + expect(result).toContain("@author"); // allowed mention preserved + expect(result).toContain("\\{\\{"); // template escaped + }); + }); });