fix: symlink bypass in validateReadPath (MEDIUM-02)

- Always resolve to absolute path first (fixes relative path bypass)
- Use realpathSync to follow symlinks before boundary check
- Throw on non-ENOENT realpathSync failures (explicit over silent)
- Resolve SAFE_DIRECTORIES through realpathSync (macOS /tmp → /private/tmp)
- Resolve directory part for non-existent files (ENOENT with symlinked parent)
This commit is contained in:
Garry Tan
2026-03-27 22:14:02 -07:00
parent 929190c588
commit b60162ae45
+24 -9
View File
@@ -37,19 +37,34 @@ function wrapForEvaluate(code: string): string {
}
// Security: Path validation to prevent path traversal attacks
const SAFE_DIRECTORIES = [TEMP_DIR, process.cwd()];
// Resolve safe directories through realpathSync to handle symlinks (e.g., macOS /tmp → /private/tmp)
const SAFE_DIRECTORIES = [TEMP_DIR, process.cwd()].map(d => {
try { return fs.realpathSync(d); } catch { return d; }
});
export function validateReadPath(filePath: string): void {
if (path.isAbsolute(filePath)) {
const resolved = path.resolve(filePath);
const isSafe = SAFE_DIRECTORIES.some(dir => isPathWithin(resolved, dir));
if (!isSafe) {
throw new Error(`Absolute path must be within: ${SAFE_DIRECTORIES.join(', ')}`);
// Always resolve to absolute first (fixes relative path symlink bypass)
const resolved = path.resolve(filePath);
// Resolve symlinks — throw on non-ENOENT errors
let realPath: string;
try {
realPath = fs.realpathSync(resolved);
} catch (err: any) {
if (err.code === 'ENOENT') {
// File doesn't exist — resolve directory part for symlinks (e.g., /tmp → /private/tmp)
try {
const dir = fs.realpathSync(path.dirname(resolved));
realPath = path.join(dir, path.basename(resolved));
} catch {
realPath = resolved;
}
} else {
throw new Error(`Cannot resolve real path: ${filePath} (${err.code})`);
}
}
const normalized = path.normalize(filePath);
if (normalized.includes('..')) {
throw new Error('Path traversal sequences (..) are not allowed');
const isSafe = SAFE_DIRECTORIES.some(dir => isPathWithin(realPath, dir));
if (!isSafe) {
throw new Error(`Path must be within: ${SAFE_DIRECTORIES.join(', ')}`);
}
}