Repository navigation
security(transport): pin the authorizer host to --url on gRPC and REST - #814
Merged
Merged
Conversation
The gin path resolves this server's own base URL through parsers.GetHostFromRequest, which returns the operator-configured --url and ignores every request header once it is set. The gRPC transport built the same value straight from caller-supplied metadata and never consulted --url at all, so the two surfaces disagreed about the deployment's own address and the pin did not reach gRPC or the grpc-gateway REST mux in front of it. That value is load-bearing: it becomes the host of the emailed password-reset and magic-link URLs (internal/service/forgot_password.go, magic_link_login.go) and the `iss` claim of the tokens minted from them, so a caller able to choose it can have a victim emailed a genuine reset link pointing at a host the caller controls. Two locks, matching the treatment x-forwarded-for already carries for the same class of bug: - transport.hostURLFromGRPC prefers parsers.TrustedURL() over any metadata. The metadata fallback is unreachable in a shipped binary (cmd.validateAuthorizerURL refuses to start without a usable --url) and survives for library embedders and tests, where it now reads the LAST value rather than the first, since the gateway appends its own authoritative value after anything a client smuggled in. - gateway's incoming-header matcher refuses X-Authorizer-Url, which closes the Grpc-Metadata- spelling DefaultHeaderMatcher forwards prefix-stripped. The gateway's own value is injected via runtime.WithMetadata and does not pass through the matcher, so it is unaffected. - parsers.TrustedURL() exported so a transport with no *http.Request can reach the pin. Both locks are mutation-verified: disabling either one alone makes its regression test fail.
lakhansamani
added a commit
that referenced
this pull request
Oct 14, 2026
Sixteen PRs merged since the 2.4.1 tag; [Unreleased] documented three of them and none of the five advisory fixes. A security release whose notes omit its security fixes gives nobody a reason to upgrade, which is the operator-facing half of the failure SECURITY.md triage exists to prevent. Renames [Unreleased] to 2.4.2 and documents all of it: - Security: the six advisories closed (#814, #799, #798, #796 x2, #815) and the three reachable dependency advisories (#797, #807, #811), plus the Go 1.27 builder (#800). - Fixed: the audit-filter parity bugs (#802) and the Cassandra index probe that never waited (#810). - Changed: direct-gRPC x-authorizer-url now yields to --url (#814), and the dropped Cassandra audit_log(created_at) index (#812). - Added: the audit plumbing (#806), stated as writing no new records and changing no behaviour, plus two deployment entries. - Upgrade notes: eight items, including the three that were missing entirely — that ANY two audit filters now scan on Cassandra (not only the timestamp range, which the previous note implied), the longer first start on ScyllaDB now that probes genuinely block, and the manual DROP INDEX for the removed index. Section order leads with Security, matching 2.4.1 — the file orders by what matters for the release at hand rather than a fixed sequence. Deliberately omitted: #791 (scorecard action bump) and #809 (the previous changelog edit). Neither is user-facing; Keep a Changelog asks for notable changes, not a commit log. The ScyllaDB view-build note is scoped to ScyllaDB: global secondary indexes are materialized views there, while Apache Cassandra builds local 2i with no view-build step and no nodetool viewbuildstatus.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Makes the operator-configured
--urlauthoritative on the gRPC and grpc-gateway REST transports, as it already is on the gin/HTTP path.Background
parsers.GetHostFromRequestresolves this server's own base URL and returns--urlin preference to every request header once it is set. That pinning is what makes the host trustworthy, and--urlis mandatory — the binary refuses to start without a usable one (cmd.validateAuthorizerURL).The gRPC transport did not go through that function.
MetaFromGRPCbuiltRequestMetadata.HostURLdirectly from caller-supplied metadata and never consulted--url, so the pin did not reach gRPC or the REST mux in front of it, and the three surfaces disagreed about the same deployment's own address.That value is load-bearing rather than cosmetic:
internal/service/forgot_password.go:74andmagic_link_login.go:157takemeta.HostURLas the host of the emailed password-reset / magic-link URL, store it on the verification request, and it becomes theissclaim of the tokens minted on redemption.Changes
Two independent locks, matching what
x-forwarded-foralready carries for the same class of bug — its comment reads "Either alone would close it; a header this load-bearing gets both."internal/grpcsrv/transport/grpc_metadata.go— newhostURLFromGRPCprefersparsers.TrustedURL()over any metadata value.The metadata fallback below it is unreachable in a shipped binary and survives for library embedders and tests, which construct a
Configdirectly. There it now reads the last value rather than the first, for the same reasonclientIPFromGRPCdoes: the gateway appends its own authoritative value after anything a client supplied, so the first entry is the caller's and the last is ours.internal/gateway/mount.go— the incoming-header matcher now refusesX-Authorizer-Url, alongside the three forwarding headers already there.DefaultHeaderMatcherforwards aGrpc-Metadata--prefixed header with the prefix stripped, so without this a REST caller could reach the metadata key the transport reads. The existingswitchcanonicalises and strips the prefix, so one entry covers every spelling.This does not affect the gateway's own
x-authorizer-urlvalue, which is injected viaruntime.WithMetadata— a separate option that operates on the already-parsed request and never passes through the matcher. There is a test asserting that, because it is the thing a future "cleanup" would break (mount.go's own comment explains why: without the gateway's value, REST-minted tokens gethttp://bufconnas their issuer and JWT validation rejects every one).internal/parsers/url.go—TrustedURL()exported, so a transport with no*http.Requestcan reach the pin.Compatibility
No change on the REST path. The gateway already injects
x-authorizer-url = parsers.GetHostFromRequest(r), which returns the trusted URL first. The valueMetaFromGRPCcomputes after this change is byte-identical to what it computed before.Direct gRPC changes behaviour, deliberately. A caller-supplied
x-authorizer-urlmetadata value used to win; now--urldoes. The only deployment that notices is one doing per-request host resolution over gRPC — and that deployment is already inconsistent, since the gin path has ignored the header ever since--urlbecame authoritative there. This makes the transports agree.X-Authorizer-URLis not documented inREADME.md,MIGRATION.mdordocs/as a deployment mechanism, so a deliberate dependency is unlikely; worth a changelog note under Changed regardless.Checked the two tests that touch this header and neither is affected:
internal/parsers/url_test.go:176deliberately preserves legacy header priority when no--urlis configured (untouched — this does not change the gin path), andinternal/integration_tests/scim_deprovision_test.gobuilds a rawgin.New()router that never reaches the gateway mux.Testing
internal/grpcsrv/transport/host_url_pin_test.go(new) and an added case ininternal/gateway/header_matcher_test.go, 7 cases total: the direct-gRPC path, value-ordering, thegrpcgateway-prefixed spelling, every blocked spelling at the gateway boundary, and three fallback behaviours including the:authoritydefault.Both locks are mutation-verified independently — each was disabled on its own, in a form that still compiled, and its regression test confirmed to fail before being restored. (Disabling lock 1 naively only produces an unused-import build error, which demonstrates nothing; the check was redone so the test itself was what failed.)
Full bar, all green locally:
Smoke was run because this touches transport wiring, per AGENTS.md — it boots the real binary and exercises every public surface, which is exactly where a wrong fix here would show up.