diff --git a/actions/setup/js/sanitize_content.cjs b/actions/setup/js/sanitize_content.cjs index 71489a3368..e99e5b7d21 100644 --- a/actions/setup/js/sanitize_content.cjs +++ b/actions/setup/js/sanitize_content.cjs @@ -96,10 +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. + // Neutralize markdown link titles as a hidden/steganographic injection channel analogous to + // HTML comments: inline-link titles are made visible in link text, while reference-style + // titles are stripped. 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) diff --git a/actions/setup/js/sanitize_content.test.cjs b/actions/setup/js/sanitize_content.test.cjs index 18f323795f..50019db154 100644 --- a/actions/setup/js/sanitize_content.test.cjs +++ b/actions/setup/js/sanitize_content.test.cjs @@ -378,6 +378,31 @@ describe("sanitize_content.cjs", () => { const result = sanitizeContent('[text](https://github.com "@exploituser inject payload")'); expect(result).toBe("[text (`@exploituser` inject payload)](https://github.com)"); }); + + it("should neutralize markdown link titles when allowedAliases is specified (XPIA regression)", () => { + // Regression: neutralizeMarkdownLinkTitles must run in the allowedAliases branch too. + // Previously the title was passed through unchanged when allowedAliases were provided. + // The title is moved into the visible link text (no longer steganographic), not stripped. + const result = sanitizeContent('[Result](https://github.com "XPIA: inject")', { allowedAliases: ["author"] }); + expect(result).toBe("[Result (XPIA: inject)](https://github.com)"); + }); + + it("should strip reference-style link titles when allowedAliases is specified", () => { + const result = sanitizeContent('[x][ref]\n\n[ref]: https://github.com "hidden payload"', { + allowedAliases: ["author"], + }); + expect(result).not.toContain("hidden payload"); + expect(result).toBe("[x][ref]\n\n[ref]: https://github.com"); + }); + + it("should neutralize link title @mentions via allowedAliases path without exposing the title steganographically", () => { + // The title @mention must be moved into visible link text and then selectively filtered. + // The allowed alias should remain un-neutralized after being moved to visible text. + const result = sanitizeContent('[text](https://github.com "@author inject")', { + allowedAliases: ["author"], + }); + expect(result).toBe("[text (@author inject)](https://github.com)"); + }); }); describe("XML/HTML tag conversion", () => {