Skip to content

Canonicalization accepts five malformed authority shapes the spec requires be rejected; request_target_uri_malformed unimplemented #978

Description

@KonstantinMirin

Found while vendoring the complete AdCP 3.1.1 request-signing vector set (#975). Reproductions are canonicalization.json cases, currently strict-xfail in tests/conformance/signing/test_canonicalization.py.

Problem

The spec requires signers to refuse to sign, and verifiers to reject, URLs whose authority is syntactically malformed:

If the authority has userinfo but no host (https://user@/p), no host at all (https://:443/p or https:///p), a bracketed host missing a closing bracket (https://[::1/p), or a bare IPv6 address outside brackets (https://fe80::1/p), the URL is malformed; signers MUST NOT sign it and verifiers MUST reject with request_target_uri_malformed.
security.mdx, @target-uri canonicalization step 3

IPv6 zone identifiers (RFC 6874) MUST be rejected. ... verifiers MUST reject @target-uri values containing %25 inside [...] with request_target_uri_malformed.
— ibid., step 2

canonicalize_target_uri() rejects only one of the six shapes. The rest are canonicalized into a well-formed-looking result and signed.

Failing cases

case input expected actual
malformed-port-without-host https://:443/p reject accepted as https:///p
malformed-userinfo-without-host https://user@/p reject accepted as https:///p
malformed-empty-authority https:///p reject accepted as https:///p
malformed-bare-ipv6 https://fe80::1/p reject accepted as https://fe80::1/p
malformed-ipv6-zone-identifier https://[fe80::1%25eth0]/p reject accepted unchanged
malformed-ipv6-missing-closing-bracket https://[::1/p reject ✅ rejected (ValueError)

The first three collapse to an empty authority, so @authority enters the signature base as the empty string and @target-uri as https:///p. Two different malformed inputs (https://:443/p and https://user@/p) canonicalize to the identical base — signatures become transferable between them.

malformed-bare-ipv6 is a distinct hazard: _canon_authority() splits fe80::1 on the last colon, yielding host fe80: and port 1, then reassembles it as fe80::1. The round-trip masks the malformation.

request_target_uri_malformed is not implemented

The spec's error code for all of these does not exist anywhere in the SDK — src/adcp/signing/errors.py has no such constant, and grep -rn request_target_uri_malformed src/ is empty. It has its own row in the transport error taxonomy:

| Request @target-uri is syntactically malformed (e.g., empty authority, bare IPv6, IPv6 zone identifier, raw non-ASCII host), OR canonicalized @authority does not byte-match the authority component of the canonical @target-uri (cross-vhost replay) | No | request_target_uri_malformed |

Because the code is missing, the vendored test asserts only that canonicalization refuses (pytest.raises(ValueError)) rather than grading the code. That assertion should be tightened to the spec code once it exists.

Note on the taxonomy row

That same row also covers a second obligation the SDK does not appear to implement: canonicalized @authority must byte-match the authority component of the canonical @target-uri, and verifiers must derive @authority from :authority/Host rather than proxy state. The spec flags it as a cross-vhost replay defense. No vector in the 3.1.1 set exercises it, so it is not covered by the strict-xfail ledger — flagging it here rather than filing separately, since the fix lands in the same place.

Suggested direction

Reject the five shapes in _canon_authority(), add request_target_uri_malformed to the error taxonomy, and map the canonicalization refusal onto it at the verifier boundary.

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