Security release. Fixes two reported advisories plus two further issues of the same classes found while fixing them.
Upgrading from 2.4.0 is recommended for every deployment. Read the migration note below first — it affects anyone running behind a reverse proxy.
Security fixes
| Issue | Severity | Affected |
|---|---|---|
| Admin-secret brute-force lockout bypass via spoofable client IP (GHSA-93hc-xq3w-xw87) | Critical | >= 2.4.0-rc.16 |
| Token-exchange delegation laundered a machine identity into a user identity (GHSA-vq29-8q3c-3hrm) | High | 2.3.0-rc.10, >= 2.4.0-rc.0 |
required_relations evaluated a machine token as a user |
High | 2.3.0-rc.10, >= 2.4.0-rc.0 |
Client IP is now taken from the connection, not from headers
utils.GetIP returned X-Real-Ip / X-Forwarded-For verbatim while the router was hardened with SetTrustedProxies. The admin-secret lockout buckets on that value, so rotating a header per request turned 10 failures per 15 minutes into unlimited online guessing of the super-admin secret.
Two further bypasses of the same counter are closed alongside it: RemoteAddr was returned as IP:port, so the ephemeral port gave every new TCP connection a fresh bucket with no header forgery at all; and the gRPC/REST surface resolved the address from caller-settable metadata, including the Grpc-Metadata-X-Forwarded-For spelling that grpc-gateway forwards with the prefix stripped.
Machine identities are no longer laundered into user identities
A delegated token minted from a service-account subject dropped its login_method claim, and the authorization resolver treats a missing claim as a human user — so an autonomous machine was evaluated as user:<sub>, flipping OpenFGA decisions from deny to allow. This was not limited to the reported self-delegation shape; the multi-hop agent chain laundered identically.
Separately, enforceRequiredRelations discarded the classified subject and hardcoded user:<id>, so validate_jwt_token with required_relations answered a plain client_credentials token against a user subject while check_permissions denied the identical token. No delegation was required to reach that one. Subject classification is now shared by all three authorization decision surfaces.
⚠️ Migration note — --trusted-proxies
--trusted-proxies now governs client-IP resolution everywhere, not only the rate limiter.
If you run behind a reverse proxy, CDN or load balancer and have not set it, every client will now resolve to the proxy's address — audit-log IPs change, and one attacker's failed admin guesses fill the single shared lockout bucket. The flag was already load-bearing for rate limiting, so this makes resolution consistent rather than newly dependent, but it must now be set.
--trusted-proxies=10.0.0.0/8,203.0.113.7/32
List every hop. With a CDN in front of a load balancer, both must appear, or the client IP resolves to the outermost unlisted proxy. Railway deployments want --trusted-proxies=100.64.0.0/10. The server logs a warning at startup when the flag is empty.
Behaviour changes
- A machine-subject delegated token now resolves to
service_account:<client_id>rather thanuser:<service-account-row-id>. Tuples written against the latter for a service account stop matching — that is the vulnerability closing. User-subject delegation is unchanged. - A chained machine-subject token exchange now succeeds past the first hop, where it previously failed with
invalid_grant. Liveness is still re-checked per hop, scope still attenuates monotonically, and the depth cap is unchanged.
Full changelog: https://github.com/authorizerdev/authorizer/blob/main/CHANGELOG.md
Compare: 2.4.0...2.4.1