Compare commits

..

1 Commits

Author SHA1 Message Date
Claude
80591ffc11 fix: prevent path traversal in delete_files MCP tool
The delete_files tool only validated absolute paths starting with "/",
allowing relative paths with traversal sequences like "./../../sensitive"
to bypass validation.

Now all paths are normalized using path.resolve() before validation,
ensuring both absolute and relative paths with ".." sequences are
properly blocked from accessing files outside the repository root.
2025-12-12 00:14:12 +00:00
6 changed files with 21 additions and 92 deletions

View File

@@ -15,20 +15,6 @@ import { GITHUB_SERVER_URL } from "../github/api/config";
import { checkAndCommitOrDeleteBranch } from "../github/operations/branch-cleanup";
import { updateClaudeComment } from "../github/operations/comments/update-claude-comment";
/**
* Encodes a branch name for use in a URL, preserving forward slashes.
* GitHub expects literal slashes in branch names (e.g., /tree/feature/branch)
* but other special characters like parentheses need to be encoded.
* Note: encodeURIComponent doesn't encode ( ) ! ' * ~ per RFC 3986,
* but parentheses break markdown links so we encode them manually.
*/
function encodeBranchName(branchName: string): string {
return encodeURIComponent(branchName)
.replace(/%2F/gi, "/")
.replace(/\(/g, "%28")
.replace(/\)/g, "%29");
}
async function run() {
try {
const commentId = parseInt(process.env.CLAUDE_COMMENT_ID!);
@@ -154,7 +140,7 @@ async function run() {
const prBody = encodeURIComponent(
`This PR addresses ${entityType.toLowerCase()} #${context.entityNumber}\n\nGenerated with [Claude Code](https://claude.ai/code)`,
);
const prUrl = `${serverUrl}/${owner}/${repo}/compare/${encodeBranchName(baseBranch)}...${encodeBranchName(claudeBranch)}?quick_pull=1&title=${prTitle}&body=${prBody}`;
const prUrl = `${serverUrl}/${owner}/${repo}/compare/${baseBranch}...${claudeBranch}?quick_pull=1&title=${prTitle}&body=${prBody}`;
prLink = `\n[Create a PR](${prUrl})`;
}
} catch (error) {

View File

@@ -2,20 +2,6 @@ import type { Octokits } from "../api/client";
import { GITHUB_SERVER_URL } from "../api/config";
import { $ } from "bun";
/**
* Encodes a branch name for use in a URL, preserving forward slashes.
* GitHub expects literal slashes in branch names (e.g., /tree/feature/branch)
* but other special characters like parentheses need to be encoded.
* Note: encodeURIComponent doesn't encode ( ) ! ' * ~ per RFC 3986,
* but parentheses break markdown links so we encode them manually.
*/
function encodeBranchName(branchName: string): string {
return encodeURIComponent(branchName)
.replace(/%2F/gi, "/")
.replace(/\(/g, "%28")
.replace(/\)/g, "%29");
}
export async function checkAndCommitOrDeleteBranch(
octokit: Octokits,
owner: string,
@@ -94,7 +80,7 @@ export async function checkAndCommitOrDeleteBranch(
);
// Set branch link since we now have commits
const branchUrl = `${GITHUB_SERVER_URL}/${owner}/${repo}/tree/${encodeBranchName(claudeBranch)}`;
const branchUrl = `${GITHUB_SERVER_URL}/${owner}/${repo}/tree/${claudeBranch}`;
branchLink = `\n[View branch](${branchUrl})`;
} else {
console.log(
@@ -105,7 +91,7 @@ export async function checkAndCommitOrDeleteBranch(
} catch (gitError) {
console.error("Error checking/committing changes:", gitError);
// If we can't check git status, assume the branch might have changes
const branchUrl = `${GITHUB_SERVER_URL}/${owner}/${repo}/tree/${encodeBranchName(claudeBranch)}`;
const branchUrl = `${GITHUB_SERVER_URL}/${owner}/${repo}/tree/${claudeBranch}`;
branchLink = `\n[View branch](${branchUrl})`;
}
} else {
@@ -116,13 +102,13 @@ export async function checkAndCommitOrDeleteBranch(
}
} else {
// Only add branch link if there are commits
const branchUrl = `${GITHUB_SERVER_URL}/${owner}/${repo}/tree/${encodeBranchName(claudeBranch)}`;
const branchUrl = `${GITHUB_SERVER_URL}/${owner}/${repo}/tree/${claudeBranch}`;
branchLink = `\n[View branch](${branchUrl})`;
}
} catch (error) {
console.error("Error comparing commits on Claude branch:", error);
// If we can't compare but the branch exists remotely, include the branch link
const branchUrl = `${GITHUB_SERVER_URL}/${owner}/${repo}/tree/${encodeBranchName(claudeBranch)}`;
const branchUrl = `${GITHUB_SERVER_URL}/${owner}/${repo}/tree/${claudeBranch}`;
branchLink = `\n[View branch](${branchUrl})`;
}
}

View File

@@ -1,19 +1,5 @@
import { GITHUB_SERVER_URL } from "../api/config";
/**
* Encodes a branch name for use in a URL, preserving forward slashes.
* GitHub expects literal slashes in branch names (e.g., /tree/feature/branch)
* but other special characters like parentheses need to be encoded.
* Note: encodeURIComponent doesn't encode ( ) ! ' * ~ per RFC 3986,
* but parentheses break markdown links so we encode them manually.
*/
function encodeBranchName(branchName: string): string {
return encodeURIComponent(branchName)
.replace(/%2F/gi, "/")
.replace(/\(/g, "%28")
.replace(/\)/g, "%29");
}
export type ExecutionDetails = {
total_cost_usd?: number;
duration_ms?: number;
@@ -174,7 +160,7 @@ export function updateCommentBody(input: CommentUpdateInput): string {
// Extract owner/repo from jobUrl
const repoMatch = jobUrl.match(/github\.com\/([^\/]+)\/([^\/]+)\//);
if (repoMatch) {
branchUrl = `${GITHUB_SERVER_URL}/${repoMatch[1]}/${repoMatch[2]}/tree/${encodeBranchName(finalBranchName)}`;
branchUrl = `${GITHUB_SERVER_URL}/${repoMatch[1]}/${repoMatch[2]}/tree/${finalBranchName}`;
}
}
@@ -186,9 +172,8 @@ export function updateCommentBody(input: CommentUpdateInput): string {
}
// Add PR link (either from content or provided)
// Use greedy match with end anchor to capture full URL even if it contains parentheses
const prUrl =
prLinkFromContent || (prLink ? prLink.match(/\((.+)\)$/)?.[1] : "");
prLinkFromContent || (prLink ? prLink.match(/\(([^)]+)\)/)?.[1] : "");
if (prUrl) {
links += ` • [Create PR ➔](${prUrl})`;
}

View File

@@ -12,26 +12,12 @@ export function createJobRunLink(
return `[View job run](${jobRunUrl})`;
}
/**
* Encodes a branch name for use in a URL, preserving forward slashes.
* GitHub expects literal slashes in branch names (e.g., /tree/feature/branch)
* but other special characters like parentheses need to be encoded.
* Note: encodeURIComponent doesn't encode ( ) ! ' * ~ per RFC 3986,
* but parentheses break markdown links so we encode them manually.
*/
function encodeBranchName(branchName: string): string {
return encodeURIComponent(branchName)
.replace(/%2F/gi, "/")
.replace(/\(/g, "%28")
.replace(/\)/g, "%29");
}
export function createBranchLink(
owner: string,
repo: string,
branchName: string,
): string {
const branchUrl = `${GITHUB_SERVER_URL}/${owner}/${repo}/tree/${encodeBranchName(branchName)}`;
const branchUrl = `${GITHUB_SERVER_URL}/${owner}/${repo}/tree/${branchName}`;
return `\n[View branch](${branchUrl})`;
}

View File

@@ -4,7 +4,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { readFile, stat } from "fs/promises";
import { join } from "path";
import { join, resolve, sep } from "path";
import { constants } from "fs";
import fetch from "node-fetch";
import { GITHUB_API_URL } from "../github/api/config";
@@ -474,20 +474,21 @@ server.tool(
throw new Error("GITHUB_TOKEN environment variable is required");
}
// Convert absolute paths to relative if they match CWD
// Normalize all paths and validate they're within the repository root
const cwd = process.cwd();
const processedPaths = paths.map((filePath) => {
if (filePath.startsWith("/")) {
if (filePath.startsWith(cwd)) {
// Strip CWD from absolute path
return filePath.slice(cwd.length + 1);
} else {
throw new Error(
`Path '${filePath}' must be relative to repository root or within current working directory`,
);
}
// Normalize the path to resolve any .. or . sequences
const normalizedPath = resolve(cwd, filePath);
// Validate the normalized path is within the current working directory
if (!normalizedPath.startsWith(cwd + sep)) {
throw new Error(
`Path '${filePath}' resolves outside the repository root`,
);
}
return filePath;
// Convert to relative path by stripping the cwd prefix
return normalizedPath.slice(cwd.length + 1);
});
// 1. Get the branch reference (create if doesn't exist)

View File

@@ -139,21 +139,6 @@ describe("updateCommentBody", () => {
);
expect(result).not.toContain("View branch");
});
it("encodes special characters in branch names while preserving slashes", () => {
const input = {
...baseInput,
branchName: "feature/fix(issue)-test",
};
const result = updateCommentBody(input);
// Branch name display should show the original name
expect(result).toContain("`feature/fix(issue)-test`");
// URL should have encoded parentheses but preserved slashes
expect(result).toContain(
"https://github.com/owner/repo/tree/feature/fix%28issue%29-test",
);
});
});
describe("PR link", () => {