mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-01-22 14:24:13 +08:00
* Add GitHub token redaction to update_claude_comment tool - Add redactGitHubTokens() function to sanitizer.ts that detects and redacts all GitHub token formats (ghp_, gho_, ghs_, ghr_, github_pat_) - Update sanitizeContent() to include token redaction in the sanitization pipeline - Apply sanitization to comment body in github-comment-server.ts before updating comments - Add comprehensive tests covering all token formats, edge cases, and integration scenarios - Prevents accidental exposure of GitHub tokens in PR/issue comments while preserving existing functionality 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * Add GitHub token redaction to inline comment server - Apply sanitizeContent() to comment body in github-inline-comment-server.ts before creating inline PR comments - Ensures consistency in token redaction across all comment creation tools - Prevents GitHub tokens from being exposed in inline PR review comments 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
364 lines
12 KiB
TypeScript
364 lines
12 KiB
TypeScript
import { describe, expect, it } from "bun:test";
|
|
import {
|
|
stripInvisibleCharacters,
|
|
stripMarkdownImageAltText,
|
|
stripMarkdownLinkTitles,
|
|
stripHiddenAttributes,
|
|
normalizeHtmlEntities,
|
|
sanitizeContent,
|
|
stripHtmlComments,
|
|
redactGitHubTokens,
|
|
} from "../src/github/utils/sanitizer";
|
|
|
|
describe("stripInvisibleCharacters", () => {
|
|
it("should remove zero-width characters", () => {
|
|
expect(stripInvisibleCharacters("Hello\u200BWorld")).toBe("HelloWorld");
|
|
expect(stripInvisibleCharacters("Text\u200C\u200D")).toBe("Text");
|
|
expect(stripInvisibleCharacters("\uFEFFStart")).toBe("Start");
|
|
});
|
|
|
|
it("should remove control characters", () => {
|
|
expect(stripInvisibleCharacters("Hello\u0000World")).toBe("HelloWorld");
|
|
expect(stripInvisibleCharacters("Text\u001F\u007F")).toBe("Text");
|
|
});
|
|
|
|
it("should preserve common whitespace", () => {
|
|
expect(stripInvisibleCharacters("Hello\nWorld")).toBe("Hello\nWorld");
|
|
expect(stripInvisibleCharacters("Tab\there")).toBe("Tab\there");
|
|
expect(stripInvisibleCharacters("Carriage\rReturn")).toBe(
|
|
"Carriage\rReturn",
|
|
);
|
|
});
|
|
|
|
it("should remove soft hyphens", () => {
|
|
expect(stripInvisibleCharacters("Soft\u00ADHyphen")).toBe("SoftHyphen");
|
|
});
|
|
|
|
it("should remove Unicode direction overrides", () => {
|
|
expect(stripInvisibleCharacters("Text\u202A\u202BMore")).toBe("TextMore");
|
|
expect(stripInvisibleCharacters("\u2066Isolated\u2069")).toBe("Isolated");
|
|
});
|
|
});
|
|
|
|
describe("stripMarkdownImageAltText", () => {
|
|
it("should remove alt text from markdown images", () => {
|
|
expect(stripMarkdownImageAltText("")).toBe(
|
|
"",
|
|
);
|
|
expect(
|
|
stripMarkdownImageAltText("Text  more text"),
|
|
).toBe("Text  more text");
|
|
});
|
|
|
|
it("should handle multiple images", () => {
|
|
expect(stripMarkdownImageAltText(" ")).toBe(
|
|
" ",
|
|
);
|
|
});
|
|
|
|
it("should handle empty alt text", () => {
|
|
expect(stripMarkdownImageAltText("")).toBe("");
|
|
});
|
|
});
|
|
|
|
describe("stripMarkdownLinkTitles", () => {
|
|
it("should remove titles from markdown links", () => {
|
|
expect(stripMarkdownLinkTitles('[Link](url.com "example title")')).toBe(
|
|
"[Link](url.com)",
|
|
);
|
|
expect(stripMarkdownLinkTitles("[Link](url.com 'example title')")).toBe(
|
|
"[Link](url.com)",
|
|
);
|
|
});
|
|
|
|
it("should handle multiple links", () => {
|
|
expect(
|
|
stripMarkdownLinkTitles('[One](1.com "first") [Two](2.com "second")'),
|
|
).toBe("[One](1.com) [Two](2.com)");
|
|
});
|
|
|
|
it("should preserve links without titles", () => {
|
|
expect(stripMarkdownLinkTitles("[Link](url.com)")).toBe("[Link](url.com)");
|
|
});
|
|
});
|
|
|
|
describe("stripHiddenAttributes", () => {
|
|
it("should remove alt attributes", () => {
|
|
expect(
|
|
stripHiddenAttributes('<img alt="example text" src="pic.jpg">'),
|
|
).toBe('<img src="pic.jpg">');
|
|
expect(stripHiddenAttributes("<img alt='example' src=\"pic.jpg\">")).toBe(
|
|
'<img src="pic.jpg">',
|
|
);
|
|
expect(stripHiddenAttributes('<img alt=example src="pic.jpg">')).toBe(
|
|
'<img src="pic.jpg">',
|
|
);
|
|
});
|
|
|
|
it("should remove title attributes", () => {
|
|
expect(
|
|
stripHiddenAttributes('<a title="example text" href="#">Link</a>'),
|
|
).toBe('<a href="#">Link</a>');
|
|
expect(stripHiddenAttributes("<div title='example'>Content</div>")).toBe(
|
|
"<div>Content</div>",
|
|
);
|
|
});
|
|
|
|
it("should remove aria-label attributes", () => {
|
|
expect(
|
|
stripHiddenAttributes('<button aria-label="example">Click</button>'),
|
|
).toBe("<button>Click</button>");
|
|
});
|
|
|
|
it("should remove data-* attributes", () => {
|
|
expect(
|
|
stripHiddenAttributes(
|
|
'<div data-test="example" data-info="more example">Text</div>',
|
|
),
|
|
).toBe("<div>Text</div>");
|
|
});
|
|
|
|
it("should remove placeholder attributes", () => {
|
|
expect(
|
|
stripHiddenAttributes('<input placeholder="example text" type="text">'),
|
|
).toBe('<input type="text">');
|
|
});
|
|
|
|
it("should handle multiple attributes", () => {
|
|
expect(
|
|
stripHiddenAttributes(
|
|
'<img alt="example" title="test" src="pic.jpg" class="image">',
|
|
),
|
|
).toBe('<img src="pic.jpg" class="image">');
|
|
});
|
|
});
|
|
|
|
describe("normalizeHtmlEntities", () => {
|
|
it("should decode numeric entities", () => {
|
|
expect(normalizeHtmlEntities("Hello")).toBe(
|
|
"Hello",
|
|
);
|
|
expect(normalizeHtmlEntities("ABC")).toBe("ABC");
|
|
});
|
|
|
|
it("should decode hex entities", () => {
|
|
expect(normalizeHtmlEntities("Hello")).toBe(
|
|
"Hello",
|
|
);
|
|
expect(normalizeHtmlEntities("ABC")).toBe("ABC");
|
|
});
|
|
|
|
it("should remove non-printable entities", () => {
|
|
expect(normalizeHtmlEntities("�")).toBe("");
|
|
expect(normalizeHtmlEntities("�")).toBe("");
|
|
});
|
|
|
|
it("should preserve normal text", () => {
|
|
expect(normalizeHtmlEntities("Normal text")).toBe("Normal text");
|
|
});
|
|
});
|
|
|
|
describe("sanitizeContent", () => {
|
|
it("should apply all sanitization measures", () => {
|
|
const testContent = `
|
|
<!-- This is a comment -->
|
|
<img alt="example alt text" src="image.jpg">
|
|

|
|
[click here](https://example.com "example title")
|
|
<div data-prompt="example data" aria-label="example label">
|
|
Normal text with hidden\u200Bcharacters
|
|
</div>
|
|
Hidden message
|
|
`;
|
|
|
|
const sanitized = sanitizeContent(testContent);
|
|
|
|
expect(sanitized).not.toContain("<!-- This is a comment -->");
|
|
expect(sanitized).not.toContain("example alt text");
|
|
expect(sanitized).not.toContain("example image description");
|
|
expect(sanitized).not.toContain("example title");
|
|
expect(sanitized).not.toContain("example data");
|
|
expect(sanitized).not.toContain("example label");
|
|
expect(sanitized).not.toContain("\u200B");
|
|
expect(sanitized).not.toContain("alt=");
|
|
expect(sanitized).not.toContain("data-prompt=");
|
|
expect(sanitized).not.toContain("aria-label=");
|
|
|
|
expect(sanitized).toContain("Normal text with hiddencharacters");
|
|
expect(sanitized).toContain("Hidden message");
|
|
expect(sanitized).toContain('<img src="image.jpg">');
|
|
expect(sanitized).toContain("");
|
|
expect(sanitized).toContain("[click here](https://example.com)");
|
|
});
|
|
|
|
it("should handle complex nested patterns", () => {
|
|
const complexContent = `
|
|
Text with  and more.
|
|
<a href="#" title="example\u00ADtitle">Link</a>
|
|
<div data-x="Hi">Content</div>
|
|
`;
|
|
|
|
const sanitized = sanitizeContent(complexContent);
|
|
|
|
expect(sanitized).not.toContain("\u200B");
|
|
expect(sanitized).not.toContain("\u00AD");
|
|
expect(sanitized).not.toContain("alt ");
|
|
expect(sanitized).not.toContain('title="');
|
|
expect(sanitized).not.toContain('data-x="');
|
|
expect(sanitized).toContain("");
|
|
expect(sanitized).toContain('<a href="#">Link</a>');
|
|
});
|
|
|
|
it("should preserve legitimate markdown and HTML", () => {
|
|
const legitimateContent = `
|
|
# Heading
|
|
|
|
This is **bold** and *italic* text.
|
|
|
|
Here's a normal image: 
|
|
And a normal link: [Click here](https://example.com)
|
|
|
|
<div class="container">
|
|
<p id="para">Normal paragraph</p>
|
|
<input type="text" name="field">
|
|
</div>
|
|
`;
|
|
|
|
const sanitized = sanitizeContent(legitimateContent);
|
|
|
|
expect(sanitized).toBe(legitimateContent);
|
|
});
|
|
|
|
it("should handle entity-encoded text", () => {
|
|
const encodedText = `
|
|
Hidden message
|
|
<div title="example">Test</div>
|
|
`;
|
|
|
|
const sanitized = sanitizeContent(encodedText);
|
|
|
|
expect(sanitized).toContain("Hidden message");
|
|
expect(sanitized).not.toContain('title="');
|
|
expect(sanitized).toContain("<div>Test</div>");
|
|
});
|
|
});
|
|
|
|
describe("redactGitHubTokens", () => {
|
|
it("should redact personal access tokens (ghp_)", () => {
|
|
const token = "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";
|
|
expect(redactGitHubTokens(`Token: ${token}`)).toBe(
|
|
"Token: [REDACTED_GITHUB_TOKEN]",
|
|
);
|
|
expect(redactGitHubTokens(`Here's a token: ${token} in text`)).toBe(
|
|
"Here's a token: [REDACTED_GITHUB_TOKEN] in text",
|
|
);
|
|
});
|
|
|
|
it("should redact OAuth tokens (gho_)", () => {
|
|
const token = "gho_16C7e42F292c6912E7710c838347Ae178B4a";
|
|
expect(redactGitHubTokens(`OAuth: ${token}`)).toBe(
|
|
"OAuth: [REDACTED_GITHUB_TOKEN]",
|
|
);
|
|
});
|
|
|
|
it("should redact installation tokens (ghs_)", () => {
|
|
const token = "ghs_xz7yzju2SZjGPa0dUNMAx0SH4xDOCS31LXQW";
|
|
expect(redactGitHubTokens(`Install token: ${token}`)).toBe(
|
|
"Install token: [REDACTED_GITHUB_TOKEN]",
|
|
);
|
|
});
|
|
|
|
it("should redact refresh tokens (ghr_)", () => {
|
|
const token = "ghr_1B4a2e77838347a253e56d7b5253e7d11667";
|
|
expect(redactGitHubTokens(`Refresh: ${token}`)).toBe(
|
|
"Refresh: [REDACTED_GITHUB_TOKEN]",
|
|
);
|
|
});
|
|
|
|
it("should redact fine-grained tokens (github_pat_)", () => {
|
|
const token =
|
|
"github_pat_11ABCDEFG0example5of9_2nVwvsylpmOLboQwTPTLewDcE621dQ0AAaBBCCDDEEFFHH";
|
|
expect(redactGitHubTokens(`Fine-grained: ${token}`)).toBe(
|
|
"Fine-grained: [REDACTED_GITHUB_TOKEN]",
|
|
);
|
|
});
|
|
|
|
it("should handle tokens in code blocks", () => {
|
|
const content = `\`\`\`bash
|
|
export GITHUB_TOKEN=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
|
|
\`\`\``;
|
|
const expected = `\`\`\`bash
|
|
export GITHUB_TOKEN=[REDACTED_GITHUB_TOKEN]
|
|
\`\`\``;
|
|
expect(redactGitHubTokens(content)).toBe(expected);
|
|
});
|
|
|
|
it("should handle multiple tokens in one text", () => {
|
|
const content =
|
|
"Token 1: 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 and token 2: gho_16C7e42F292c6912E7710c838347Ae178B4a";
|
|
expect(redactGitHubTokens(content)).toBe(
|
|
"Token 1: [REDACTED_GITHUB_TOKEN] and token 2: [REDACTED_GITHUB_TOKEN]",
|
|
);
|
|
});
|
|
|
|
it("should handle tokens in URLs", () => {
|
|
const content =
|
|
"https://api.github.com/user?access_token=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";
|
|
expect(redactGitHubTokens(content)).toBe(
|
|
"https://api.github.com/user?access_token=[REDACTED_GITHUB_TOKEN]",
|
|
);
|
|
});
|
|
|
|
it("should not redact partial matches or invalid tokens", () => {
|
|
const content =
|
|
"This is not a token: ghp_short or gho_toolong1234567890123456789012345678901234567890";
|
|
expect(redactGitHubTokens(content)).toBe(content);
|
|
});
|
|
|
|
it("should preserve normal text", () => {
|
|
const content = "Normal text with no tokens";
|
|
expect(redactGitHubTokens(content)).toBe(content);
|
|
});
|
|
|
|
it("should handle edge cases", () => {
|
|
expect(redactGitHubTokens("")).toBe("");
|
|
expect(redactGitHubTokens("ghp_")).toBe("ghp_");
|
|
expect(redactGitHubTokens("github_pat_short")).toBe("github_pat_short");
|
|
});
|
|
});
|
|
|
|
describe("sanitizeContent with token redaction", () => {
|
|
it("should redact tokens as part of full sanitization", () => {
|
|
const content = `
|
|
<!-- Hidden comment with token: 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 -->
|
|
Here's some text with a token: gho_16C7e42F292c6912E7710c838347Ae178B4a
|
|
And invisible chars: test\u200Btoken
|
|
`;
|
|
|
|
const sanitized = sanitizeContent(content);
|
|
|
|
expect(sanitized).not.toContain("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");
|
|
expect(sanitized).not.toContain("gho_16C7e42F292c6912E7710c838347Ae178B4a");
|
|
expect(sanitized).not.toContain("<!-- Hidden comment");
|
|
expect(sanitized).not.toContain("\u200B");
|
|
expect(sanitized).toContain("[REDACTED_GITHUB_TOKEN]");
|
|
expect(sanitized).toContain("Here's some text with a token:");
|
|
});
|
|
});
|
|
|
|
describe("stripHtmlComments (legacy)", () => {
|
|
it("should remove HTML comments", () => {
|
|
expect(stripHtmlComments("Hello <!-- example -->World")).toBe(
|
|
"Hello World",
|
|
);
|
|
expect(stripHtmlComments("<!-- comment -->Text")).toBe("Text");
|
|
expect(stripHtmlComments("Text<!-- comment -->")).toBe("Text");
|
|
});
|
|
|
|
it("should handle multiline comments", () => {
|
|
expect(stripHtmlComments("Hello <!-- \nexample\n -->World")).toBe(
|
|
"Hello World",
|
|
);
|
|
});
|
|
});
|