(inputPath: string)
| 286 | * @returns An array of absolute paths to check permissions for |
| 287 | */ |
| 288 | export function getPathsForPermissionCheck(inputPath: string): string[] { |
| 289 | // Expand tilde notation defensively - tools should do this in getPath(), |
| 290 | // but we normalize here as defense in depth for permission checking |
| 291 | let path = inputPath |
| 292 | if (path === '~') { |
| 293 | path = homedir().normalize('NFC') |
| 294 | } else if (path.startsWith('~/')) { |
| 295 | path = nodePath.join(homedir().normalize('NFC'), path.slice(2)) |
| 296 | } |
| 297 | |
| 298 | const pathSet = new Set<string>() |
| 299 | const fsImpl = getFsImplementation() |
| 300 | |
| 301 | // Always check the original path |
| 302 | pathSet.add(path) |
| 303 | |
| 304 | // Block UNC paths before any filesystem access to prevent network |
| 305 | // requests (DNS/SMB) during validation on Windows |
| 306 | if (path.startsWith('//') || path.startsWith('\\\\')) { |
| 307 | return Array.from(pathSet) |
| 308 | } |
| 309 | |
| 310 | // Follow the symlink chain, collecting ALL intermediate targets |
| 311 | // This handles cases like: test.txt -> /etc/passwd -> /private/etc/passwd |
| 312 | // We want to check all three paths, not just test.txt and /private/etc/passwd |
| 313 | try { |
| 314 | let currentPath = path |
| 315 | const visited = new Set<string>() |
| 316 | const maxDepth = 40 // Prevent runaway loops, matches typical SYMLOOP_MAX |
| 317 | |
| 318 | for (let depth = 0; depth < maxDepth; depth++) { |
| 319 | // Prevent infinite loops from circular symlinks |
| 320 | if (visited.has(currentPath)) { |
| 321 | break |
| 322 | } |
| 323 | visited.add(currentPath) |
| 324 | |
| 325 | if (!fsImpl.existsSync(currentPath)) { |
| 326 | // Path doesn't exist (new file case). existsSync follows symlinks, |
| 327 | // so this is also reached for DANGLING symlinks (link entry exists, |
| 328 | // target doesn't). Resolve symlinks in the path and its ancestors |
| 329 | // so permission checks see the real destination. Without this, |
| 330 | // `./data -> /etc/cron.d/` (live parent symlink) or |
| 331 | // `./evil.txt -> ~/.ssh/authorized_keys2` (dangling file symlink) |
| 332 | // would allow writes that escape the working directory. |
| 333 | if (currentPath === path) { |
| 334 | const resolved = resolveDeepestExistingAncestorSync(fsImpl, path) |
| 335 | if (resolved !== undefined) { |
| 336 | pathSet.add(resolved) |
| 337 | } |
| 338 | } |
| 339 | break |
| 340 | } |
| 341 | |
| 342 | const stats = fsImpl.lstatSync(currentPath) |
| 343 | |
| 344 | // Skip special file types that can cause issues |
| 345 | if ( |
no test coverage detected