File size: 1,911 Bytes
7421850 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | /**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import * as fs from 'node:fs';
import * as path from 'node:path';
import { resolveToRealPath, isSubpath } from '@google/gemini-cli-core';
/**
* Validates a workspace path to prevent path traversal attacks.
*
* @param workspacePath The path to validate.
* @param allowedRoot The root directory the path must be within. Defaults to CWD.
* @returns The resolved, safe path.
* @throws An error if the path is invalid or outside the allowed root.
*/
export async function validateWorkspacePath(
workspacePath?: string,
allowedRoot: string = process.cwd(),
): Promise<string> {
const trimmedPath = workspacePath?.trim();
if (!trimmedPath) {
return resolveToRealPath(allowedRoot);
}
if (trimmedPath.includes('\0')) {
throw new Error('Security violation: Null byte detected in path.');
}
try {
const canonicalAllowedRoot = resolveToRealPath(allowedRoot);
const resolvedWorkspacePath = path.resolve(
canonicalAllowedRoot,
trimmedPath,
);
const canonicalWorkspacePath = resolveToRealPath(resolvedWorkspacePath);
// Check if the resolved path is within the allowed root directory
if (
canonicalWorkspacePath !== canonicalAllowedRoot &&
!isSubpath(canonicalAllowedRoot, canonicalWorkspacePath)
) {
throw new Error(
`Security violation: The path "${trimmedPath}" is outside the allowed root directory.`,
);
}
const stats = await fs.promises.stat(canonicalWorkspacePath);
if (!stats.isDirectory()) {
throw new Error(`The path "${trimmedPath}" is not a directory.`);
}
return canonicalWorkspacePath;
} catch (e) {
if (e instanceof Error && 'code' in e && e.code === 'ENOENT') {
throw new Error(`The path "${trimmedPath}" does not exist.`);
}
throw e; // Re-throw other errors
}
}
|