(filePath: string)
| 31 | |
| 32 | /** Validate a file path for writing (screenshot, pdf, download, scrape, archive). */ |
| 33 | export function validateOutputPath(filePath: string): void { |
| 34 | const resolved = path.resolve(filePath); |
| 35 | |
| 36 | // If the target already exists and is a symlink, resolve through it. |
| 37 | // Without this, a symlink at /tmp/evil.png → /etc/crontab passes the |
| 38 | // parent-directory check (parent is /tmp, which is safe) but the actual |
| 39 | // write follows the symlink to /etc/crontab. |
| 40 | try { |
| 41 | const stat = fs.lstatSync(resolved); |
| 42 | if (stat.isSymbolicLink()) { |
| 43 | const realTarget = fs.realpathSync(resolved); |
| 44 | const isSafe = SAFE_DIRECTORIES.some(dir => isPathWithin(realTarget, dir)); |
| 45 | if (!isSafe) { |
| 46 | throw new Error(`Path must be within: ${SAFE_DIRECTORIES.join(', ')}`); |
| 47 | } |
| 48 | return; // symlink target verified, no need to check parent |
| 49 | } |
| 50 | } catch (e: any) { |
| 51 | // ENOENT = file doesn't exist yet, fall through to parent-dir check |
| 52 | if (e.code !== 'ENOENT') throw e; |
| 53 | } |
| 54 | |
| 55 | // For new files (no existing symlink), verify the parent directory. |
| 56 | // The file itself may not exist yet (e.g., screenshot output). |
| 57 | // This also handles macOS /tmp → /private/tmp transparently. |
| 58 | let dir = path.dirname(resolved); |
| 59 | let realDir: string; |
| 60 | try { |
| 61 | realDir = fs.realpathSync(dir); |
| 62 | } catch { |
| 63 | try { |
| 64 | realDir = fs.realpathSync(path.dirname(dir)); |
| 65 | } catch { |
| 66 | throw new Error(`Path must be within: ${SAFE_DIRECTORIES.join(', ')}`); |
| 67 | } |
| 68 | } |
| 69 | |
| 70 | const realResolved = path.join(realDir, path.basename(resolved)); |
| 71 | const isSafe = SAFE_DIRECTORIES.some(dir => isPathWithin(realResolved, dir)); |
| 72 | if (!isSafe) { |
| 73 | throw new Error(`Path must be within: ${SAFE_DIRECTORIES.join(', ')}`); |
| 74 | } |
| 75 | } |
| 76 | |
| 77 | /** Validate a file path for reading (eval command). */ |
| 78 | export function validateReadPath(filePath: string): void { |
no test coverage detected