Files
rengine/shared/utils/net.py
T
Yogesh OjhaandClaude Sonnet 5 7a960f1de1 fix(net): a malformed URL port is rejected, not raised, everywhere .port is read
urllib.parse.SplitResult.port raises ValueError when the authority
carries a non-numeric port; nine call sites read it unguarded, so one
malformed URL (a stray quote, a client typo) turned into an unhandled
exception in the middle of a scan, a report, an interest rule or a
proxy dial. shared/utils/net.py:url_port makes the read itself the
failure boundary; unreadable_port names the bogus text so the naabu
and banner proxy paths can report what they refused rather than
retrying against a host string that dropped its port silently.

shared/definitions/ports.py:SCHEME_PORTS replaces four private copies
of the same http/https default-port map (endpoints.py, endpoint_judge,
scan_surface/normalize, nuclei's parser, dalfox's parser).

Also renames live_hosts to answered_hosts in the report source and the
interest context, and lifts the asset query's inline is:web predicate
into predicates.answered() — the existing distinction between is:live
(2xx) and is:web (answered at all) was already the rule; live_hosts
was the one name still claiming the stronger one.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VckaDx6mBB4TsG76u61gvv
2026-09-16 08:04:06 +05:30

101 lines
3.2 KiB
Python

import ipaddress
import socket
from collections.abc import Iterable
from urllib.parse import SplitResult, urlsplit
def is_registry_routable(value: str) -> bool:
"""Whether an address or netblock sits in public space an RIR can hold a record for."""
try:
net = ipaddress.ip_network(value.strip(), strict=False)
except ValueError:
return False
return net.is_global and not net.is_multicast
def validate_public_https_url(raw: str, *, label: str = "URL") -> None:
parts = urlsplit(raw)
if parts.scheme != "https" or not parts.hostname:
msg = f"{label} must be an https:// URL."
raise ValueError(msg)
declared = unreadable_port(parts)
if declared:
msg = f"{label} carries {declared!r}, which is not a port number."
raise ValueError(msg)
try:
infos = socket.getaddrinfo(parts.hostname, None)
except OSError as exc:
msg = f"Cannot resolve {label} host: {exc}"
raise ValueError(msg) from exc
for info in infos:
ip = ipaddress.ip_address(info[4][0])
if (
ip.is_private
or ip.is_loopback
or ip.is_link_local
or ip.is_reserved
or ip.is_multicast
or ip.is_unspecified
):
msg = f"{label} resolves to a disallowed address."
raise ValueError(msg)
def cert_covers(
host: str | None, subject_cn: str | None, sans: Iterable[str] = ()
) -> bool:
"""Whether a certificate names the host presenting it."""
if not host:
return False
target = host.strip().lower().rstrip(".")
for raw in (subject_cn, *sans):
if not raw:
continue
name = str(raw).strip().lower().rstrip(".")
if name == target:
return True
if (
name.startswith("*.")
and target.endswith(name[1:])
and target.count(".") == name.count(".")
):
return True
return False
def url_port(parts: SplitResult) -> int | None:
"""A URL's port, or None when absent or unreadable: reading .port is what raises."""
try:
return parts.port
except ValueError:
return None
def unreadable_port(parts: SplitResult) -> str | None:
"""The authority's port text when it is not a port number, else None."""
_, declared = split_host_port(parts.netloc.rsplit("@", 1)[-1])
return declared if declared and url_port(parts) is None else None
def bracketed(host: str) -> str:
"""An IPv6 literal in brackets. Anything else is returned as it is."""
return f"[{host}]" if ":" in host and not host.startswith("[") else host
def host_port(host: str, port: int | str) -> str:
"""An authority a tool can parse."""
return f"{bracketed(host)}:{port}"
def split_host_port(authority: str) -> tuple[str, str | None]:
"""The host and port of an authority, with an IPv6 literal unwrapped."""
value = authority.strip()
if value.startswith("["):
literal, _, rest = value.partition("]")
port = rest[1:] if rest.startswith(":") else ""
return literal[1:], port or None
if value.count(":") == 1:
host, _, port = value.partition(":")
return host, port or None
return value, None