diff --git a/src/entrypoints/post-buffered-inline-comments.ts b/src/entrypoints/post-buffered-inline-comments.ts index 763dae7f..9283baf6 100644 --- a/src/entrypoints/post-buffered-inline-comments.ts +++ b/src/entrypoints/post-buffered-inline-comments.ts @@ -11,6 +11,7 @@ */ import { readFileSync } from "fs"; import { createOctokit } from "../github/api/client"; +import { redactSecrets } from "../github/utils/sanitizer"; const BUFFER_PATH = "/tmp/inline-comments-buffer.jsonl"; @@ -120,7 +121,7 @@ async function postComment( owner, repo, pull_number, - body: c.body, + body: redactSecrets(c.body), path: c.path, side: c.side || "RIGHT", commit_id: c.commit_id || headSha, diff --git a/src/mcp/github-comment-server.ts b/src/mcp/github-comment-server.ts index ef6728c9..8195eed9 100644 --- a/src/mcp/github-comment-server.ts +++ b/src/mcp/github-comment-server.ts @@ -6,7 +6,7 @@ import { z } from "zod"; import { GITHUB_API_URL } from "../github/api/config"; import { Octokit } from "@octokit/rest"; 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 const REPO_OWNER = process.env.REPO_OWNER; @@ -55,7 +55,7 @@ server.tool( const isPullRequestReviewComment = eventName === "pull_request_review_comment"; - const sanitizedBody = sanitizeContent(body); + const sanitizedBody = redactSecrets(sanitizeContent(body)); const result = await updateClaudeComment(octokit, { owner, diff --git a/src/mcp/github-inline-comment-server.ts b/src/mcp/github-inline-comment-server.ts index a023d911..129fc40e 100644 --- a/src/mcp/github-inline-comment-server.ts +++ b/src/mcp/github-inline-comment-server.ts @@ -4,7 +4,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js" import { appendFileSync } from "fs"; import { z } from "zod"; 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"; // Get repository and PR information from environment variables @@ -98,8 +98,8 @@ server.tool( const repo = REPO_NAME; const pull_number = parseInt(PR_NUMBER, 10); - // Sanitize the comment body to remove any potential GitHub tokens - const sanitizedBody = sanitizeContent(body); + // Sanitize the comment body to remove potential prompt injections and redact secrets + const sanitizedBody = redactSecrets(sanitizeContent(body)); // Validate that either line or both startLine and line are provided if (!line && !startLine) { diff --git a/test/public-comment-redaction.test.ts b/test/public-comment-redaction.test.ts new file mode 100644 index 00000000..4ab47086 --- /dev/null +++ b/test/public-comment-redaction.test.ts @@ -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", + "", + "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( + "", + ); + 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]"); + }); +}); diff --git a/test/sanitizer.test.ts b/test/sanitizer.test.ts index ef68a496..7b935e51 100644 --- a/test/sanitizer.test.ts +++ b/test/sanitizer.test.ts @@ -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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 "; + 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]"); + }); +});