Repository navigation
Security release. Closes six advisories reported through SECURITY.md, plus three
dependency advisories reachable from this codebase. Upgrading from 2.4.1 is
recommended for every deployment. See the upgrade notes at the end — audit-log
result counts change on two backends, and one behaviour changes on the gRPC and
REST transports.
Security
- Trusted-URL pin was bypassed on the gRPC and REST transports (GHSA-m46m-j9p5-5j99, high).
--urlis authoritative for this server's own base URL on the gin path, insideparsers.GetHostFromRequest. The gRPC transport built the same value straight from caller-supplied metadata and never consulted--url, so the pin reached neither gRPC nor the grpc-gateway REST mux in front of it. That value becomes the host of the emailed password-reset and magic-link URL and theissclaim of the tokens minted from it, so a caller able to choose it could have a victim emailed a genuine reset link pointing at a host the caller controls. Fixed with two independent locks, matching the treatmentx-forwarded-foralready carries: the transport now prefers the configured--url, and the gateway refuses theX-Authorizer-Urlheader (closing theGrpc-Metadata-spelling thatDefaultHeaderMatcherforwards prefix-stripped). Affected>= 2.3.0-rc.3(#814). - Public gRPC methods skipped the delegated-scope gate (GHSA-25j3-4jg3-w352, high). The auth interceptor returned for every
publicmethod before resolving any credential, soenforceDelegatedScope— which sat below that return — never ran on the public path. Six public RPCs are dual-mode and hand a bearer token to a resolver that accepts a delegated token, so an agent holding anopenid-scoped delegated token could callTotpMfaSetup, collect a Verified MFA-session cookie, and trade it atSkipMfaSetupfor an unattenuated first-party user token with noactclaim. No MFA RPC is on the delegated allow-list, so the policy already said deny; it was never consulted. Credential resolution on the public path stays opportunistic — a caller with no credential reaches the handler exactly as before — and the gate no-ops on first-party tokens. Machine identities are now also rejected at the two dual-mode resolvers. Affected>= 2.4.0-rc.13(#799). /verify_emaildelivered session tokens in the redirect query string (GHSA-44vr-f829-xfch, high). The handler appendedaccess_token,id_tokenandrefresh_tokento the query string of a redirect target validated only at origin granularity, so any path under an allowed origin received a full session. An unauthenticated attacker could start the flow throughmagic_link_loginwith aredirect_uriunder the app's own origin and read the victim's tokens from the target page, the access log, the browser history or theRefererheader — and because this endpoint is reached from an emailed link, that URL is unusually long-lived. The tokens are gone; redirect validation additionally goes throughcheckClientRedirectURI, the exact-match validator/authorizealready uses. Affected all releases through 2.4.1 (#798).- A password-only profile change did not revoke existing sessions (GHSA-qx8r-9p3g-vcp4, medium).
DeleteAllUserSessionslived only insideUpdateProfile's email-change branch, so a user who changed only their password kept every pre-existing session and refresh token alive — a stolen refresh token continued minting access tokens after the victim believed they had locked the attacker out. The siblingreset_passwordpath already revoked correctly. Affected all releases through 2.4.1 (#796). validate_jwt_tokendid not bind the token to its signedtoken_type(GHSA-cqr5-83v5-mx8f, medium). The requested type was checked against a list of legal names but never compared with the token's own signed claim, so all six cross-type combinations validated: a refresh token or anid_tokenpassed as an access token. The session-store lookup did not catch it because one nonce is stamped on all three tokens of a login, so an entry existed whatever token was presented, and the fetched value was discarded rather than compared. Both checks are restored, matchingValidateAccessTokenandValidateRefreshToken. The rejection is indistinguishable from any other invalid token, since a distinct error on a public RPC would disclose an unknown token's type. Reachable on GraphQL, gRPC and REST, which share one implementation. Affected all releases through 2.4.1 (#796)./authorize/consentaccepted a delegated token with no scope check (GHSA-hhhh-p743-q3cp, low). The handler authorized an approval on caller identity alone, and the dual-mode resolver returns the delegating user'ssubfor an RFC 8693 delegated token, so the check passed for an agent acting on a user's behalf — on a plain gin route behind neither of the two gates that enforceinternal/delegatedscope. Anopenid-scoped delegated token could therefore record a consent approval for an arbitrary third-party client. The caller cannot redeem it (/authorizeresolves its session from the cookie only, and the login-handoff code it can observe is not bound to an authenticated user and fails exchange), so the exposure is that consent can be pre-approved for one exact pending request and a later visit from the user's own browser then skips the consent screen. Delegated callers are now refused outright: approving a grant confers whatever scope the third-party client requested, unbounded by the agent's own ceiling, so the operation is deliberately absent from the delegated allow-list rather than gated by a scope. Browser flows are unchanged — the session cookie is resolved before any bearer token. Affected all releases through 2.4.1 (#815).- gRPC bumped to v1.83.2 for GO-2026-6443 (server panic via missing
authority/Hostheaders, reached bygrpcsrv.Run→grpc.Server.Serve→http2Server.HandleStreams) and GO-2026-6348 (reached through the generated admin gRPC client). Both reachable from called code, not merely present. Note the target: the advisory names v1.82.2 as the fix for the 1.82.x line, but on 1.83.x it landed in v1.83.2, so v1.83.1 is still affected (#797). - OpenTelemetry bumped to v1.45.0 for GO-2026-6505: the OTLP trace exporter can log exporter configuration — including endpoint URLs, which may carry credentials or internal hostnames — at info level.
govulncheckclasses it reachable here through the embedded OpenFGA engine's init, the gocql driver and the metrics package. Indirect dependency only; no source change (#807). - golang.org/x/net bumped to v0.60.0 for five called vulnerabilities — GO-2026-6617, GO-2026-6612, GO-2026-6611, GO-2026-6610 (HTTP/2 framing and header handling) and GO-2026-6603 (HTTP/2 server memory exhaustion via Trailer headers). All reachable: the traces run through the gRPC listener, which reads and writes h2 frames directly, the outbound webhook test client, and WebAuthn registration verification (#811).
- Release image builds with Go 1.27 so the shipped binary picks up the current toolchain and its standard-library security fixes.
go.moddeliberately stays at 1.26.6 — Go builds an older module with a newer toolchain unchanged, so this is a toolchain bump, not a language version bump (#800).
Fixed
- Audit-log filters were silently ignored on CassandraDB/ScyllaDB and Couchbase.
_audit_logsadvertises six filters; those two backends built theirWHEREclause fromactor_idandactiononly and discardedresource_type,resource_id,from_timestampandto_timestamp— returning unfiltered rows with no error, so a narrowed audit query read as authoritative when it was not. The dashboard's Audit Logs page was already sending three of the dropped filters, so the UI offered filtering that did nothing. A second bug surfaced while fixing it: on ScyllaDB two indexed equalities withoutALLOW FILTERINGare rejected outright, which is the query the old builder emitted — so_audit_logs(action:, actor_id:), the one combination Cassandra was believed to support, had never worked there. All six filters now apply on every backend (#802). - A failed audit-log scan returned a truncated page with a nil error on CassandraDB/ScyllaDB.
ListAuditLogsnever checkedscanner.Err(), so a read timeout or coordinator failure part-way through ended iteration normally: the caller got fewer rows and no indication, whilepagination.totalreported the real count. Newly reachable because the filters above can now produce scans, where a partial read is far likelier (#802). - Couchbase could fail to start after an upgrade while building a newly added secondary index. The per-attempt query timeout was the client default (75s) while the retry budget was 30s, so one attempt outran the whole deadline and the error aborted provider construction; separately, the transient-error check matched only
"unambiguous timeout"and so missed gocb's"ambiguous timeout"— which is exactly what aCREATE INDEXthat outruns the client returns, since the server may have accepted it. Both are fixed, and a timed-out attempt is now harmless: the definition is registered server-side, the retry sees it already exists, and startup proceeds while the backfill finishes (#802). - The Cassandra/ScyllaDB secondary-index readiness probe never waited.
waitForCassandraSecondaryIndexexisted to block startup until an asynchronously-built index became queryable, but its probe carriedALLOW FILTERING— precisely the hint that lets Cassandra answer a query on a non-indexed column. The probe therefore succeeded whether or not the index existed and returned on its first attempt, leaving all 13 call sites exposed to the startup race it was written to close: an intermittent index-unavailable error on the first query against a freshly provisioned keyspace. The hint is gone, and the probes are consolidated into one wait against a single bounded budget (#810).
Changed
- A caller-supplied
x-authorizer-urlis now ignored on direct gRPC in favour of the configured--url. Previously the metadata value won on that transport. The gin/HTTP path has ignored the equivalent header since--urlbecame authoritative, so this makes the transports agree rather than introducing a new restriction; the REST path is unaffected, since the gateway already injected the trusted value. A deployment doing per-request host resolution over gRPC must switch to--url(#814). - The Cassandra
audit_log(created_at)index is no longer created. A Cassandra secondary index serves equality only, and every query this table issues againstcreated_atis a range — theListAuditLogsbounds andDeleteAuditLogsBefore, both already on the scan path — so the index could never be used while still costing a write on every audit row, on the highest-write table in the schema. TheALLOW FILTERINGdecision is now derived from an explicit indexed-column set rather than the clause count, so the hint cannot be omitted for a column that has no index. Couchbase keeps itscreated_atGSI, which N1QL does use for the range predicates and theORDER BY created_at DESCon every list query (#812).
Added
- Groundwork for authorization-change evidence.
audit.ProvidergainsLogEventSync, a synchronous audit write that returns its error, for operations where the audit record is part of the contract rather than a side effect;LogEventkeeps its fire-and-forget semantics so logins and token issuance do not gain a synchronous dependency on the audit table.token.ProvidergainsAdminAuthMode, which reports whether a super-admin authenticated by admin session or by shared secret — super-admin is a single sharedAdminSecretwith no per-admin identity, so an audit record can never name a person, and the credential mode is the honest substitute. The SCIM service gains an audit provider, which it previously lacked entirely. No new audit records are written and no behaviour changes in this release; this is the plumbing the next phase consumes (#806). - One-click deployment entries for ZopDay (#801) and Railyard (#808).
Upgrade notes
- CassandraDB/ScyllaDB and Couchbase: audit-log result counts will drop. The four previously-discarded filters now apply. Any saved query, dashboard view or integration that passed them was receiving unfiltered rows; it will now receive correctly filtered ones, and
pagination.totalchanges with them. This is a correction, not data loss. - CassandraDB/ScyllaDB: any two or more audit filters combined now use
ALLOW FILTERINGand perform a scan. Not only the timestamp range: one query can be served by one index, so a second restriction requires the hint._audit_logs(action:, actor_id:)is an ordinary admin search and it now scans — acceptable for an admin-only paginated endpoint, and strictly better than the pre-2.4.2 behaviour where that combination errored outright on ScyllaDB, but it is a scan and will slow as the table grows. - ScyllaDB: two new secondary indexes backfill after upgrade.
resource_typeandresource_idfilters can return incomplete results until the backfill finishes — ScyllaDB builds global secondary indexes as materialized views. Verify withnodetool viewbuildstatusbefore treating a filtered audit query as authoritative, and expect elevated cluster I/O while it runs. (Apache Cassandra builds local 2i instead, with no view-build step.) - ScyllaDB: first start after upgrade may take longer. Index readiness probes now genuinely block, where previously they returned immediately without waiting. The total is bounded at 90 seconds across all probes and is normally a fraction of that on a warm cluster; a cold, freshly provisioned keyspace is the slow case.
- CassandraDB/ScyllaDB: the
audit_log(created_at)index is no longer created, and existing deployments keep theirs. It is unused — a 2i cannot serve the range queries this column gets — so it is pure write amplification. Drop it by hand when convenient; nothing breaks if you leave it:DROP INDEX IF EXISTS authorizer.authorizer_audit_log_created_at;
- Couchbase: index builds may show activity for a while after upgrade. Two new GSIs are created against the existing audit-log collection. No action required — startup no longer blocks on the build.
- Direct gRPC callers setting
x-authorizer-urlmust switch to--url. See Changed above. REST and HTTP callers are unaffected. - No GraphQL, gRPC/proto or REST response shape changed. No SDK release is required for this version.