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(?!`)/);
+ });
});