diff --git a/.changeset/patch-sanitize-safe-output-handlers.md b/.changeset/patch-sanitize-safe-output-handlers.md new file mode 100644 index 0000000000..074a7b408c --- /dev/null +++ b/.changeset/patch-sanitize-safe-output-handlers.md @@ -0,0 +1,5 @@ +--- +"gh-aw": patch +--- + +Sanitize user-provided content in all affected safe-output handlers so workflow markers and footers survive the SEC-004 workflow, fixing the injection-risk regression described in PR #15805. diff --git a/actions/setup/js/add_comment.cjs b/actions/setup/js/add_comment.cjs index a789bed2a7..7e63996219 100644 --- a/actions/setup/js/add_comment.cjs +++ b/actions/setup/js/add_comment.cjs @@ -14,6 +14,7 @@ const { resolveTarget } = require("./safe_output_helpers.cjs"); const { resolveTargetRepoConfig, resolveAndValidateRepo } = require("./repo_helpers.cjs"); const { getMissingInfoSections } = require("./missing_messages_helper.cjs"); const { getMessages } = require("./messages_core.cjs"); +const { sanitizeContent } = require("./sanitize_content.cjs"); /** @type {string} Safe output type handled by this module */ const HANDLER_TYPE = "add_comment"; @@ -463,6 +464,9 @@ async function main(config = {}) { // Replace temporary ID references in body let processedBody = replaceTemporaryIdReferences(item.body || "", temporaryIdMap, itemRepo); + // Sanitize content to prevent injection attacks + processedBody = sanitizeContent(processedBody); + // Enforce max limits before processing (validates user-provided content) try { enforceCommentLimits(processedBody); diff --git a/actions/setup/js/add_comment.test.cjs b/actions/setup/js/add_comment.test.cjs index 7e9e84cc78..921492a8be 100644 --- a/actions/setup/js/add_comment.test.cjs +++ b/actions/setup/js/add_comment.test.cjs @@ -1098,6 +1098,122 @@ describe("add_comment", () => { expect(capturedBody).not.toContain("aw_test02"); }); }); + + describe("sanitization preserves markers", () => { + it("should preserve tracker ID markers after sanitization", async () => { + const addCommentScript = fs.readFileSync(path.join(__dirname, "add_comment.cjs"), "utf8"); + + // Setup environment + process.env.GH_AW_WORKFLOW_NAME = "Test Workflow"; + process.env.GH_AW_TRACKER_ID = "test-tracker-123"; + + let capturedBody = null; + mockGithub.rest.issues.createComment = async params => { + capturedBody = params.body; + return { + data: { + id: 12345, + html_url: "https://github.com/owner/repo/issues/42#issuecomment-12345", + }, + }; + }; + + // Execute the handler + const handler = await eval(`(async () => { ${addCommentScript}; return await main({}); })()`); + + const message = { + type: "add_comment", + body: "User content with attempt", + }; + + const result = await handler(message, {}); + + expect(result.success).toBe(true); + expect(capturedBody).toBeDefined(); + // Verify tracker ID is present (not removed by sanitization) + expect(capturedBody).toContain(""); + // Verify script tags were sanitized (converted to safe format) + expect(capturedBody).not.toContain(" and "; + const sanitizedBody = sanitizeContent(userContent); + const trackerID = getTrackerID("markdown"); + const footer = generateFooterWithMessages("Test Workflow", "https://github.com/test/repo/actions/runs/123", "test.md", "https://github.com/test/repo", 42, undefined, undefined); + const result = sanitizedBody.trim() + trackerID + footer; + + // User content should be sanitized (tags converted) + expect(result).not.toContain("