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("