fix(security): unify secret redaction in public comment outputs (#1693)

Ensure all public issue, PR, and inline comments apply redactSecrets() in addition to sanitizeContent() before submitting payloads to the GitHub API. This aligns public comment output with error log and step-summary redaction policies, preventing potential leakage of Anthropic API keys, AWS credentials, Slack tokens, JWTs, and GitHub tokens.
This commit is contained in:
Juwan
2026-08-18 17:23:11 -07:00
committed by GitHub
parent d40ddef4c0
commit 54eadc2f72
5 changed files with 96 additions and 6 deletions
@@ -11,6 +11,7 @@
*/ */
import { readFileSync } from "fs"; import { readFileSync } from "fs";
import { createOctokit } from "../github/api/client"; import { createOctokit } from "../github/api/client";
import { redactSecrets } from "../github/utils/sanitizer";
const BUFFER_PATH = "/tmp/inline-comments-buffer.jsonl"; const BUFFER_PATH = "/tmp/inline-comments-buffer.jsonl";
@@ -120,7 +121,7 @@ async function postComment(
owner, owner,
repo, repo,
pull_number, pull_number,
body: c.body, body: redactSecrets(c.body),
path: c.path, path: c.path,
side: c.side || "RIGHT", side: c.side || "RIGHT",
commit_id: c.commit_id || headSha, commit_id: c.commit_id || headSha,
+2 -2
View File
@@ -6,7 +6,7 @@ import { z } from "zod";
import { GITHUB_API_URL } from "../github/api/config"; import { GITHUB_API_URL } from "../github/api/config";
import { Octokit } from "@octokit/rest"; import { Octokit } from "@octokit/rest";
import { updateClaudeComment } from "../github/operations/comments/update-claude-comment"; import { updateClaudeComment } from "../github/operations/comments/update-claude-comment";
import { sanitizeContent } from "../github/utils/sanitizer"; import { redactSecrets, sanitizeContent } from "../github/utils/sanitizer";
// Get repository information from environment variables // Get repository information from environment variables
const REPO_OWNER = process.env.REPO_OWNER; const REPO_OWNER = process.env.REPO_OWNER;
@@ -55,7 +55,7 @@ server.tool(
const isPullRequestReviewComment = const isPullRequestReviewComment =
eventName === "pull_request_review_comment"; eventName === "pull_request_review_comment";
const sanitizedBody = sanitizeContent(body); const sanitizedBody = redactSecrets(sanitizeContent(body));
const result = await updateClaudeComment(octokit, { const result = await updateClaudeComment(octokit, {
owner, owner,
+3 -3
View File
@@ -4,7 +4,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
import { appendFileSync } from "fs"; import { appendFileSync } from "fs";
import { z } from "zod"; import { z } from "zod";
import { createOctokit } from "../github/api/client"; import { createOctokit } from "../github/api/client";
import { sanitizeContent } from "../github/utils/sanitizer"; import { redactSecrets, sanitizeContent } from "../github/utils/sanitizer";
import { removeBufferedComment } from "./inline-comment-buffer"; import { removeBufferedComment } from "./inline-comment-buffer";
// Get repository and PR information from environment variables // Get repository and PR information from environment variables
@@ -98,8 +98,8 @@ server.tool(
const repo = REPO_NAME; const repo = REPO_NAME;
const pull_number = parseInt(PR_NUMBER, 10); const pull_number = parseInt(PR_NUMBER, 10);
// Sanitize the comment body to remove any potential GitHub tokens // Sanitize the comment body to remove potential prompt injections and redact secrets
const sanitizedBody = sanitizeContent(body); const sanitizedBody = redactSecrets(sanitizeContent(body));
// Validate that either line or both startLine and line are provided // Validate that either line or both startLine and line are provided
if (!line && !startLine) { if (!line && !startLine) {
+64
View File
@@ -0,0 +1,64 @@
import { describe, expect, it } from "bun:test";
import { redactSecrets, sanitizeContent } from "../src/github/utils/sanitizer";
describe("Public Comment Output Sanitization & Redaction", () => {
it("redacts all credential types from public comment output", () => {
const rawComment = [
"Here is the summary of the work done:",
"- GitHub Token: ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890",
"- Anthropic Key: sk-ant-api03-abcdefghijklmnopqrstuvwxyz1234567890",
"- AWS Access Key: AKIAIOSFODNN7EXAMPLE",
"- Slack Bot Token: xoxb-1234567890-abcdefghijkl-mnopqrstuvwx",
"- JWT Bearer: eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U",
"<!-- Hidden instruction injection -->",
"Invisible\u200Bzero-width chars",
"![Image Alt Injection](https://example.com/pic.png)",
].join("\n");
const sanitizedOutput = redactSecrets(sanitizeContent(rawComment));
// Ensure all secret types are redacted
expect(sanitizedOutput).not.toContain(
"ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890",
);
expect(sanitizedOutput).not.toContain(
"sk-ant-api03-abcdefghijklmnopqrstuvwxyz1234567890",
);
expect(sanitizedOutput).not.toContain("AKIAIOSFODNN7EXAMPLE");
expect(sanitizedOutput).not.toContain(
"xoxb-1234567890-abcdefghijkl-mnopqrstuvwx",
);
expect(sanitizedOutput).not.toContain(
"eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U",
);
expect(sanitizedOutput).toContain("[REDACTED_GITHUB_TOKEN]");
expect(sanitizedOutput).toContain("[REDACTED_ANTHROPIC_KEY]");
expect(sanitizedOutput).toContain("[REDACTED_AWS_KEY_ID]");
expect(sanitizedOutput).toContain("[REDACTED_SLACK_TOKEN]");
expect(sanitizedOutput).toContain("[REDACTED_JWT]");
// Ensure prompt injection / invisible chars / hidden tags are also sanitized
expect(sanitizedOutput).not.toContain(
"<!-- Hidden instruction injection -->",
);
expect(sanitizedOutput).not.toContain("\u200B");
expect(sanitizedOutput).not.toContain("Image Alt Injection");
expect(sanitizedOutput).toContain("![](https://example.com/pic.png)");
});
it("ensures public comments have the same secret redaction coverage as logs/errors", () => {
const errorDetails =
"Error: failed to connect with sk-ant-abcdefghijklmnopqrstuvwxyz123456 and AKIAIOSFODNN7EXAMPLE";
const commentBody =
"Report: encountered sk-ant-abcdefghijklmnopqrstuvwxyz123456 and AKIAIOSFODNN7EXAMPLE";
const redactedError = redactSecrets(errorDetails);
const redactedComment = redactSecrets(sanitizeContent(commentBody));
expect(redactedError).toContain("[REDACTED_ANTHROPIC_KEY]");
expect(redactedError).toContain("[REDACTED_AWS_KEY_ID]");
expect(redactedComment).toContain("[REDACTED_ANTHROPIC_KEY]");
expect(redactedComment).toContain("[REDACTED_AWS_KEY_ID]");
});
});
+25
View File
@@ -518,3 +518,28 @@ describe("stripHtmlComments (legacy)", () => {
); );
}); });
}); });
describe("outbound comment sanitization and redaction", () => {
it("should sanitize content and redact all credential types for public comments", () => {
const rawComment =
"Done! Configured AWS AKIAIOSFODNN7EXAMPLE, Anthropic sk-ant-api03-abcdefghijklmnopqrstuvwxyz1234567890, Slack xoxb-1234567890-abcdefghijkl-mnopqrstuvwx, and GitHub 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 <!-- secret note -->";
const sanitizedAndRedacted = redactSecrets(sanitizeContent(rawComment));
expect(sanitizedAndRedacted).not.toContain("AKIAIOSFODNN7EXAMPLE");
expect(sanitizedAndRedacted).not.toContain(
"sk-ant-api03-abcdefghijklmnopqrstuvwxyz1234567890",
);
expect(sanitizedAndRedacted).not.toContain(
"xoxb-1234567890-abcdefghijkl-mnopqrstuvwx",
);
expect(sanitizedAndRedacted).not.toContain(
"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",
);
expect(sanitizedAndRedacted).not.toContain("secret note");
expect(sanitizedAndRedacted).toContain("[REDACTED_AWS_KEY_ID]");
expect(sanitizedAndRedacted).toContain("[REDACTED_ANTHROPIC_KEY]");
expect(sanitizedAndRedacted).toContain("[REDACTED_SLACK_TOKEN]");
expect(sanitizedAndRedacted).toContain("[REDACTED_GITHUB_TOKEN]");
});
});