Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions actions/setup/js/sanitize_content.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
25 changes: 25 additions & 0 deletions actions/setup/js/sanitize_content.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down