Skip to content

DockerLocalhostRewrite.rewrite_to is interpolated into the netloc unvalidated; a bare IPv6 value breaks webhook signing #991

Description

@KonstantinMirin

Summary

DockerLocalhostRewrite.rewrite_to is interpolated straight into a netloc with no validation. An IPv6 value produces an unbracketed authority, which is the one shape RFC 3986 makes ambiguous with a port. This is the only place a non-signing layer synthesizes the netloc that later becomes the signed @authority.

Cause

src/adcp/webhook_transport_hooks.py:97-100:

netloc = self.rewrite_to
if parsed.port is not None:
    netloc = f"{self.rewrite_to}:{parsed.port}"
return urlunsplit((parsed.scheme, netloc, parsed.path, parsed.query, parsed.fragment))

rewrite_to is a plain str dataclass field (:82) with no constraint beyond its default. The docstring suggests 172.17.0.1 for Linux hosts, so operators are already expected to put an IP literal here; nothing tells them a v6 literal needs brackets, and nothing rejects it if they omit them.

Reproduce

from adcp.webhook_transport_hooks import DockerLocalhostRewrite
from adcp.signing.canonical import canonicalize_authority

h = DockerLocalhostRewrite(rewrite_to="::1")
h.rewrite_url("https://localhost:9000/hook")
# 'https://::1:9000/hook'

canonicalize_authority("https://::1:9000/hook")
rewrite_to rewritten URL @authority
host.docker.internal https://host.docker.internal:9000/hook host.docker.internal:9000
172.17.0.1 https://172.17.0.1:9000/hook 172.17.0.1:9000
::1 https://::1:9000/hook TargetUriMalformedError
2001:db8::1 https://2001:db8::1:9000/hook TargetUriMalformedError
[::1] https://[::1]:9000/hook [::1]:9000

Bracketing it by hand works. Nothing tells the operator to.

Why it matters

The hook runs before signing — webhook_sender.py:974 calls apply_hooks, and the signer sees the rewritten URL — so the ordering is correct and the signature does cover the URL actually dialled. The problem is the value the hook hands forward.

Before #985, _canon_authority split the unbracketed netloc on its last colon and reassembled it, so ::1:9000 round-tripped and looked fine while being an ambiguous authority. After #985 the same input is a hard TargetUriMalformedError at sign time. Failing loudly is the better outcome, but it converts a silent misconfiguration into a delivery failure at first send, with the error raised in canonical.py rather than anywhere near the hook that caused it.

Scope is genuinely small: this is an opt-in e2e-testing hook that already refuses to construct unless the sender was built with allow_private_destinations=True (:102-118), and the failure is at the adopter's own configured destination. It is a config-validation gap, not a protocol defect.

Suggested direction

validate_for_sender (:102) is already the construction-time validation seam, and it already raises ValueError for the other misconfiguration this hook has. Validate rewrite_to there: reject anything that is not a valid hostname or a bracketed/bare IP literal, and bracket a bare IPv6 literal rather than rejecting it — the operator's intent is unambiguous, and the docstring actively encourages IP-literal values.

Doing it at construction keeps the error at wiring time, which is the stated design of this hook's existing check, rather than at the first delivery attempt.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions