ADR-0071: HTTP Outbound URL Policy (Query Composition and CamelHttpUri Fence)
Status
Accepted (2026-09-06). Implements the query-composition half of the
http-contract-surface change for the camel-http producer; the capability
spec openspec/changes/http-contract-surface/specs/http-url-resolution/spec.md
is normative for the scenario detail. Companion to ADR-0032 (exchange-data
trust boundary) and ADR-0034 (control-bus authorizedRoutes fence precedent).
Context
Three bd findings name the outbound URL as a contract surface under ADR-0032, which defines exchange headers as untrusted, adversary-controlled data:
- rc-rbfxq — a
CamelHttpUriexchange header replaces the entire outbound URL — scheme, host, path, query — before SSRF validation, with no allowlist tying it to the endpoint's configured host. SSRF default (allow_internal=false) blocks private addresses only, so an attacker who can setCamelHttpUriredirects the producer to any public host. The e_glm counter-report lowered it to p2 (defense-in-depth, not default-path exploitability): reachable only when a route step explicitly copies untrusted data into an exactly-casedCamelHttpUriheader. But the contract inconsistency with ADR-0034 stands — control-bus fenced the same header-driven control-target class withauthorizedRoutes, while camel-http had no fence. - rc-k3pir — default inbound reflection is undocumented: the consumer
installs
CamelHttpPath/CamelHttpQueryfrom the inbound wire request (lib.rs:1821-1825), and a non-bridged producer consumes both by default, sofrom: http://.../in -> to: http://upstream/apiappends the inbound path and query to the upstream URL with zero route steps. Through the rc-69fie else-if, the inbound query silently replaced operator-configuredquery_params. - rc-69fie — a
CamelHttpQueryheader present on an exchange carrying endpoint-configured query params took the header branch and silently discarded the configured params — no merge, no warning. Under ADR-0032 this is worse: the untrusted exchange header fully replaced trusted operator config.
ADR-0032 establishes that no untrusted exchange datum may drive a control
plane, numeric, or resource decision, or an executable/interpretable sink,
without validation, bounding, or a capability check. The outbound URL is a
resource decision. ADR-0034 supplies the fence precedent: control-bus fenced
its header-driven route target with a mandatory authorizedRoutes allowlist
that fails closed when absent.
Decision
-
Outbound query composition (rc-69fie). When the exchange carries a
CamelHttpQueryheader, the producer composes the outbound query from the arm-specific higher-precedence source followed by the header pairs whose keys are absent from that set; the higher-precedence source wins any key collision.- Base arm (no
CamelHttpUri): the higher-precedence source is the endpoint query —raw_query(consumed option keys filtered) plus programmaticquery_params. - Override arm (
CamelHttpUripresent): the higher-precedence source is the override URI's own query. The endpoint base query does NOT ride an override — the override remains untrusted exchange data under ADR-0032, so trusted operator config never rides an untrusted override. CamelHttpPathapplies to the path component before query composition in both arms.- Header pair bytes are carried verbatim; a raw byte forbidden in a query component inside a header value produces a resolve error naming the offending byte, never a re-encoding (Wave-A law, raw-preserving serializer).
- A present-but-empty
CamelHttpQueryis a no-op: the higher-precedence source is emitted unchanged with no additional?marker. - Override-URI merge fix: when an override URI carries its own query
and the exchange also carries
CamelHttpQuery, the two merge at pair level — override pairs first (winning collisions), header pairs appending for absent keys — instead of concatenating a second?marker. - Deliberate divergence from Apache Camel. Apache Camel applies the
CamelHttpQueryheader verbatim, replacing the endpoint query entirely (header-wins-verbatim). This ADR composes instead, so collisions resolve to the higher-precedence (operator) source: the endpoint config in the base arm, the override URI's own pairs in the override arm. The divergence is explicit and test-pinned.
- Base arm (no
-
Default inbound reflection retained (rc-k3pir, Apache Camel parity). Consumer-installed
CamelHttpPath/CamelHttpQueryride the outbound URL by default and compose per rule 1. The plain-proxy shape keeps working; the operator query pair is not replaced by reflected inbound data. -
Opt-in
allowedUriHostsfence (rc-rbfxq). The endpoint URI accepts a comma-separatedallowedUriHostsoption: exact hosts, each optionallyhost:port.- Bracketed IPv6 literals compare in canonical form; DNS names compare
case-insensitively; a host-only entry permits any port; a
host:portentry matches only the override's effective port (explicit port, or the scheme default: 443 https / 80 http). - A malformed entry — a segment carrying a path or userinfo, an entry the
urlcrate rejects, or a declared option that yields zero valid entries after trimming/dropping empty comma segments — fails endpoint creation. - An armed fence fails closed: a
CamelHttpUrioverride resolving to a host not matching any entry, or failing to yield a host, is a resolve error, and the rejected URL is rendered only through the diagnostics redaction path (ADR-0051). - An unarmed endpoint (option absent) keeps the pre-fence override behavior unchanged. The option is consumed: it never appears in the outbound query.
- Bracketed IPv6 literals compare in canonical form; DNS names compare
case-insensitively; a host-only entry permits any port; a
-
Compatibility exception versus ADR-0034. Unlike control-bus's MANDATORY
authorizedRoutesfence (which fails closed when absent), the camel-http fence is opt-in: unarmed endpoints keep the pre-fence override behavior. This is a deliberate compatibility trade-off — defense-in-depth hardening, not incident response — preserving Apache Camel override semantics for operators who did not ask for the fence.
Consequences
- Migration for Apache Camel users relying on header-wins-verbatim:
their
CamelHttpQueryheaders now COMPOSE. Collisions resolve to the higher-precedence source — the endpoint config in the base arm, the override URI's own pairs in the override arm — and only absent keys append. A route that previously overwrote a static operator pair must delete that pair from the endpoint URI or drop the header. - Fence adoption path for routes that copy untrusted inbound data into
CamelHttpUri: declareallowedUriHostswith the exact target hosts. An armed fence turns a redirect-to-any-public-host into a redacted resolve error. The fence is enforced at override resolution time only: redirect hops underfollowRedirects(off by default) are NOT fence-checked and rely on the existing per-hop SSRF validation and cross-origin credential stripping. - Reflection is now documented as the default, next to
bridgeEndpoint, rather than an undocumented side effect. - Wave-A law continues to govern authored and header query bytes: the raw-preserving serializer carries them verbatim, forbidden bytes error naming the byte, and the redaction path protects credentials in any diagnostic rendering of a rejected URL.
- Contract surface updated in
crates/components/camel-http/CONTEXT.md; future bug reports about outbound URL/query behavior check the composition and fence rules there first.