(url: string)
| 578 | } |
| 579 | |
| 580 | export function validateUrl(url: string): { |
| 581 | valid: boolean; |
| 582 | error?: string; |
| 583 | } { |
| 584 | if (isFileUrl(url) || isLocalPath(url)) { |
| 585 | // fileUrlToPath already decodes percent-encoding; for bare paths, |
| 586 | // decode here in case the client sends %20 for spaces etc. |
| 587 | const filePath = isFileUrl(url) |
| 588 | ? fileUrlToPath(url) |
| 589 | : decodeURIComponent(url); |
| 590 | const resolved = path.resolve(filePath); |
| 591 | |
| 592 | // Check exact match (CLI args / roots) |
| 593 | if (allowedLocalFiles.has(resolved)) { |
| 594 | if (!fs.existsSync(resolved)) { |
| 595 | return { valid: false, error: `File not found: ${resolved}` }; |
| 596 | } |
| 597 | return { valid: true }; |
| 598 | } |
| 599 | |
| 600 | // Check directory match (MCP roots / CLI dirs). |
| 601 | // Try both the raw path and its realpath (resolves symlinks). |
| 602 | let realResolved: string | undefined; |
| 603 | try { |
| 604 | realResolved = fs.realpathSync(resolved); |
| 605 | } catch { |
| 606 | // File may not exist yet at this path |
| 607 | } |
| 608 | if ( |
| 609 | [...allowedLocalDirs].some((dir) => { |
| 610 | let realDir: string | undefined; |
| 611 | try { |
| 612 | realDir = fs.realpathSync(dir); |
| 613 | } catch { |
| 614 | // Dir may not exist |
| 615 | } |
| 616 | return ( |
| 617 | isAncestorDir(dir, resolved) || |
| 618 | (realResolved != null && isAncestorDir(dir, realResolved)) || |
| 619 | (realDir != null && isAncestorDir(realDir, resolved)) || |
| 620 | (realDir != null && |
| 621 | realResolved != null && |
| 622 | isAncestorDir(realDir, realResolved)) |
| 623 | ); |
| 624 | }) |
| 625 | ) { |
| 626 | if (!fs.existsSync(resolved)) { |
| 627 | return { valid: false, error: `File not found: ${resolved}` }; |
| 628 | } |
| 629 | return { valid: true }; |
| 630 | } |
| 631 | |
| 632 | console.error( |
| 633 | `[pdf-server] Local file not in allowed list: ${resolved}\n Allowed dirs: ${[...allowedLocalDirs].join(", ")}`, |
| 634 | ); |
| 635 | return { |
| 636 | valid: false, |
| 637 | error: `Local file not in allowed list: ${resolved}\nAllowed directories: ${[...allowedLocalDirs].join(", ")}`, |
no test coverage detected
searching dependent graphs…