mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-01-28 02:42:25 +08:00
Add validatePathWithinRepo helper to ensure file paths resolve within the repository root directory. This hardens the commit_files tool by validating paths before file operations. Changes: - Add src/mcp/path-validation.ts with async path validation using realpath - Update commit_files to validate all paths before reading files - Prevent symlink-based path escapes by resolving real paths - Add comprehensive test coverage including symlink attack scenarios 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude <noreply@anthropic.com>
65 lines
2.0 KiB
TypeScript
65 lines
2.0 KiB
TypeScript
import { realpath } from "fs/promises";
|
|
import { resolve, sep } from "path";
|
|
|
|
/**
|
|
* Validates that a file path resolves within the repository root.
|
|
* Prevents path traversal attacks via "../" sequences and symlinks.
|
|
* @param filePath - The file path to validate (can be relative or absolute)
|
|
* @param repoRoot - The repository root directory
|
|
* @returns The resolved absolute path (with symlinks resolved) if valid
|
|
* @throws Error if the path resolves outside the repository root
|
|
*/
|
|
export async function validatePathWithinRepo(
|
|
filePath: string,
|
|
repoRoot: string,
|
|
): Promise<string> {
|
|
// First resolve the path string (handles .. and . segments)
|
|
const initialPath = resolve(repoRoot, filePath);
|
|
|
|
// Resolve symlinks to get the real path
|
|
// This prevents symlink attacks where a link inside the repo points outside
|
|
let resolvedRoot: string;
|
|
let resolvedPath: string;
|
|
|
|
try {
|
|
resolvedRoot = await realpath(repoRoot);
|
|
} catch {
|
|
throw new Error(`Repository root '${repoRoot}' does not exist`);
|
|
}
|
|
|
|
try {
|
|
resolvedPath = await realpath(initialPath);
|
|
} catch {
|
|
// File doesn't exist yet - fall back to checking the parent directory
|
|
// This handles the case where we're creating a new file
|
|
const parentDir = resolve(initialPath, "..");
|
|
try {
|
|
const resolvedParent = await realpath(parentDir);
|
|
if (
|
|
resolvedParent !== resolvedRoot &&
|
|
!resolvedParent.startsWith(resolvedRoot + sep)
|
|
) {
|
|
throw new Error(
|
|
`Path '${filePath}' resolves outside the repository root`,
|
|
);
|
|
}
|
|
// Parent is valid, return the initial path since file doesn't exist yet
|
|
return initialPath;
|
|
} catch {
|
|
throw new Error(
|
|
`Path '${filePath}' resolves outside the repository root`,
|
|
);
|
|
}
|
|
}
|
|
|
|
// Path must be within repo root (or be the root itself)
|
|
if (
|
|
resolvedPath !== resolvedRoot &&
|
|
!resolvedPath.startsWith(resolvedRoot + sep)
|
|
) {
|
|
throw new Error(`Path '${filePath}' resolves outside the repository root`);
|
|
}
|
|
|
|
return resolvedPath;
|
|
}
|