(
url: string | null | undefined,
paramName = 'url',
options: { allowHttp?: boolean } = {}
)
| 77 | * @returns AsyncValidationResult with resolved IP for DNS pinning |
| 78 | */ |
| 79 | export async function validateUrlWithDNS( |
| 80 | url: string | null | undefined, |
| 81 | paramName = 'url', |
| 82 | options: { allowHttp?: boolean } = {} |
| 83 | ): Promise<AsyncValidationResult> { |
| 84 | const basicValidation = validateExternalUrl(url, paramName, options) |
| 85 | if (!basicValidation.isValid) { |
| 86 | return basicValidation |
| 87 | } |
| 88 | |
| 89 | const parsedUrl = new URL(url!) |
| 90 | const hostname = parsedUrl.hostname |
| 91 | |
| 92 | const hostnameLower = hostname.toLowerCase() |
| 93 | const cleanHostname = |
| 94 | hostnameLower.startsWith('[') && hostnameLower.endsWith(']') |
| 95 | ? hostnameLower.slice(1, -1) |
| 96 | : hostnameLower |
| 97 | |
| 98 | let isLocalhost = cleanHostname === 'localhost' |
| 99 | if (ipaddr.isValid(cleanHostname)) { |
| 100 | const processedIP = ipaddr.process(cleanHostname).toString() |
| 101 | if (processedIP === '127.0.0.1' || processedIP === '::1') { |
| 102 | isLocalhost = true |
| 103 | } |
| 104 | } |
| 105 | |
| 106 | try { |
| 107 | const { address } = await dns.lookup(cleanHostname, { verbatim: true }) |
| 108 | |
| 109 | const resolvedIsLoopback = |
| 110 | ipaddr.isValid(address) && |
| 111 | (() => { |
| 112 | const ip = ipaddr.process(address).toString() |
| 113 | return ip === '127.0.0.1' || ip === '::1' |
| 114 | })() |
| 115 | |
| 116 | if (isPrivateOrReservedIP(address) && !(isLocalhost && resolvedIsLoopback && !isHosted)) { |
| 117 | logger.warn('URL resolves to blocked IP address', { |
| 118 | paramName, |
| 119 | hostname, |
| 120 | resolvedIP: address, |
| 121 | }) |
| 122 | return { |
| 123 | isValid: false, |
| 124 | error: `${paramName} resolves to a blocked IP address`, |
| 125 | } |
| 126 | } |
| 127 | |
| 128 | return { |
| 129 | isValid: true, |
| 130 | resolvedIP: address, |
| 131 | originalHostname: hostname, |
| 132 | } |
| 133 | } catch (error) { |
| 134 | logger.warn('DNS lookup failed for URL', { |
| 135 | paramName, |
| 136 | hostname, |
no test coverage detected