Check that the target URL is reachable from inside the container.
(targetUrl: string, logger: ActivityLogger)
| 495 | |
| 496 | /** Check that the target URL is reachable from inside the container. */ |
| 497 | async function validateTargetUrl(targetUrl: string, logger: ActivityLogger): Promise<Result<void, PentestError>> { |
| 498 | logger.info('Checking target URL reachability...'); |
| 499 | |
| 500 | // 1. Parse URL |
| 501 | let parsed: URL; |
| 502 | try { |
| 503 | parsed = new URL(targetUrl); |
| 504 | } catch { |
| 505 | return err( |
| 506 | new PentestError( |
| 507 | `Invalid target URL: ${targetUrl}`, |
| 508 | 'config', |
| 509 | false, |
| 510 | { targetUrl }, |
| 511 | ErrorCode.TARGET_UNREACHABLE, |
| 512 | ), |
| 513 | ); |
| 514 | } |
| 515 | |
| 516 | // 2. Resolve all records once — reused (pinned) for the connection below. |
| 517 | const hostname = parsed.hostname; |
| 518 | let addresses: LookupAddress[]; |
| 519 | try { |
| 520 | addresses = await lookup(hostname, { all: true }); |
| 521 | } catch { |
| 522 | return err( |
| 523 | new PentestError( |
| 524 | `Target URL ${targetUrl} is not reachable. Verify the URL is correct and the site is up.`, |
| 525 | 'network', |
| 526 | false, |
| 527 | { targetUrl, hostname }, |
| 528 | ErrorCode.TARGET_UNREACHABLE, |
| 529 | ), |
| 530 | ); |
| 531 | } |
| 532 | |
| 533 | // 3. Reject the link-local metadata range (169.254.0.0/16). |
| 534 | const blocked = addresses.find((entry) => isBlockedAddress(entry.address)); |
| 535 | if (blocked) { |
| 536 | return err( |
| 537 | new PentestError( |
| 538 | `Target URL ${targetUrl} resolves to ${blocked.address}, a link-local address ` + |
| 539 | `(169.254.0.0/16). This range hosts the cloud instance metadata service and cannot be scanned.`, |
| 540 | 'config', |
| 541 | false, |
| 542 | { targetUrl, hostname, address: blocked.address }, |
| 543 | ErrorCode.TARGET_UNREACHABLE, |
| 544 | ), |
| 545 | ); |
| 546 | } |
| 547 | |
| 548 | // 4. HTTP reachability check (socket pinned to the resolved addresses). |
| 549 | try { |
| 550 | await httpHead(targetUrl, TARGET_URL_TIMEOUT_MS, addresses); |
| 551 | |
| 552 | logger.info('Target URL OK'); |
| 553 | return ok(undefined); |
| 554 | } catch (error) { |
no test coverage detected