(url: URL)
| 605 | } |
| 606 | |
| 607 | private async validateFetchTarget(url: URL): Promise<string | null> { |
| 608 | if (url.protocol !== "https:") { |
| 609 | return "URL must use HTTPS"; |
| 610 | } |
| 611 | |
| 612 | if (url.username || url.password || (url.port !== "" && url.port !== "443")) { |
| 613 | return "URL must not include credentials or non-default ports"; |
| 614 | } |
| 615 | |
| 616 | const hostname = this.normalizeDomain(url.hostname); |
| 617 | if (!hostname || hostname === "localhost" || hostname.endsWith(".localhost")) { |
| 618 | return "URL must resolve to a public internet host"; |
| 619 | } |
| 620 | |
| 621 | if (this.isPrivateIpAddress(hostname)) { |
| 622 | return "URL must resolve to a public internet host"; |
| 623 | } |
| 624 | |
| 625 | try { |
| 626 | const records = await dns.lookup(hostname, { all: true, verbatim: true }); |
| 627 | if (records.length === 0 || records.some((record) => this.isPrivateIpAddress(record.address))) { |
| 628 | return "URL must resolve to a public internet host"; |
| 629 | } |
| 630 | } catch { |
| 631 | return "URL hostname could not be resolved"; |
| 632 | } |
| 633 | |
| 634 | return null; |
| 635 | } |
| 636 | |
| 637 | private isPrivateIpAddress(address: string): boolean { |
| 638 | const ipVersion = net.isIP(address); |
no test coverage detected