From f027b969a02e55701e17a80fad81a220f06a1b5d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Apr 2026 02:18:36 +0000 Subject: [PATCH 1/2] Initial plan From 1d30c6521fc2b3704729d329c455b8f5859c4680 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Apr 2026 02:27:03 +0000 Subject: [PATCH 2/2] fix: sanitize command output before embedding in issue/comment bodies (SEC-004) Import and apply the shared sanitizeContent helper to truncatedOutput before embedding it in GitHub issue/comment bodies in run_validate_workflows.cjs. This prevents untrusted subprocess output from escaping code fences or injecting unexpected Markdown/HTML. Add tests verifying @mentions in output are neutralized in both the new-issue and existing-issue-comment code paths. Agent-Logs-Url: https://github.com/github/gh-aw/sessions/b1e91242-6a52-4b62-a671-b50030ed6caf Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/run_validate_workflows.cjs | 7 ++- .../setup/js/run_validate_workflows.test.cjs | 47 +++++++++++++++++++ 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/actions/setup/js/run_validate_workflows.cjs b/actions/setup/js/run_validate_workflows.cjs index e2ca7f1ac52..ff0d3be0cec 100644 --- a/actions/setup/js/run_validate_workflows.cjs +++ b/actions/setup/js/run_validate_workflows.cjs @@ -4,6 +4,7 @@ const { getErrorMessage } = require("./error_helpers.cjs"); const { generateFooterWithMessages, generateXMLMarker } = require("./messages_footer.cjs"); const { buildWorkflowRunUrl } = require("./workflow_metadata_helpers.cjs"); +const { sanitizeContent } = require("./sanitize_content.cjs"); /** * Run full workflow validation using gh-aw compile --validate and all known @@ -89,6 +90,7 @@ async function main() { core.info(`Found existing issue #${existingIssue.number}: ${existingIssue.html_url}`); const truncatedOutput = combinedOutput.substring(0, 50000) + (combinedOutput.length > 50000 ? "\n\n... (output truncated)" : ""); + const sanitizedOutput = sanitizeContent(truncatedOutput); const xmlMarker = generateXMLMarker(workflowName, runUrl); const commentBody = `Validation still has findings (exit code: ${exitCode}). @@ -97,7 +99,7 @@ async function main() { Validation output \`\`\` -${truncatedOutput} +${sanitizedOutput} \`\`\` @@ -130,6 +132,7 @@ ${xmlMarker}`; core.info("No existing issue found, creating a new issue with validation findings"); const truncatedOutput = combinedOutput.substring(0, 50000) + (combinedOutput.length > 50000 ? "\n\n... (output truncated)" : ""); + const sanitizedOutput = sanitizeContent(truncatedOutput); const xmlMarker = generateXMLMarker(workflowName, runUrl); const issueBody = `## Problem @@ -145,7 +148,7 @@ Workflow validation found errors or warnings that need to be addressed. Full output \`\`\` -${truncatedOutput} +${sanitizedOutput} \`\`\` diff --git a/actions/setup/js/run_validate_workflows.test.cjs b/actions/setup/js/run_validate_workflows.test.cjs index 4dd59f4e6a8..e2efcaa25a9 100644 --- a/actions/setup/js/run_validate_workflows.test.cjs +++ b/actions/setup/js/run_validate_workflows.test.cjs @@ -230,4 +230,51 @@ describe("run_validate_workflows", () => { const createCall = mockGithub.rest.issues.create.mock.calls[0][0]; expect(createCall.body).toContain("... (output truncated)"); }); + + it("should sanitize output containing @mentions in new issue body", async () => { + mockExec.exec.mockImplementation(async (_cmd, _args, options) => { + if (options?.listeners?.stderr) { + options.listeners.stderr(Buffer.from("Error: @malicious-user triggered a warning\n")); + } + return 1; + }); + + mockGithub.rest.search.issuesAndPullRequests.mockResolvedValue({ + data: { total_count: 0, items: [] }, + }); + + mockGithub.rest.issues.create.mockResolvedValue({ + data: { number: 45, html_url: "https://github.com/testowner/testrepo/issues/45" }, + }); + + const { main } = await import("./run_validate_workflows.cjs"); + await main(); + + const createCall = mockGithub.rest.issues.create.mock.calls[0][0]; + // sanitizeContent should neutralize @mentions so they don't ping users + expect(createCall.body).not.toMatch(/@malicious-user(?!`)/); + }); + + it("should sanitize output containing @mentions in comment body", async () => { + mockExec.exec.mockImplementation(async (_cmd, _args, options) => { + if (options?.listeners?.stderr) { + options.listeners.stderr(Buffer.from("Error: @malicious-user triggered a warning\n")); + } + return 1; + }); + + mockGithub.rest.search.issuesAndPullRequests.mockResolvedValue({ + data: { + total_count: 1, + items: [{ number: 10, html_url: "https://github.com/testowner/testrepo/issues/10" }], + }, + }); + + const { main } = await import("./run_validate_workflows.cjs"); + await main(); + + const commentCall = mockGithub.rest.issues.createComment.mock.calls[0][0]; + // sanitizeContent should neutralize @mentions so they don't ping users + expect(commentCall.body).not.toMatch(/@malicious-user(?!`)/); + }); });