Found while reviewing #6046.
Problem
HandleTokenEndpointRequest copies the subject token's prior act claim into the newly minted one without checking it is a JSON object:
// pkg/authserver/server/tokenexchange/handler.go:156-162
if priorAct, ok := validatedClaims.Extra["act"]; ok && priorAct != nil {
if actChainDepth(priorAct) >= maxDelegationDepth {
return errorsx.WithStack(fosite.ErrInvalidGrant.WithHint(
"The subject token's delegation chain is too deep."))
}
act["act"] = priorAct // <-- no type check
}
The depth gate does not catch it either, because actChainDepth returns 0 for anything that is not a map:
// pkg/authserver/server/tokenexchange/handler.go:475-489
func actChainDepth(act any) int {
depth := 0
for {
m, ok := act.(map[string]any)
if !ok {
return depth // <-- non-object => depth 0, gate passes
}
...
RFC 8693 §4.1 states: "The act claim value is a JSON object, and members in the JSON object are claims that identify the actor." So a subject token carrying e.g. "act": "some-agent" or an array produces a minted token with act: {"sub": "<actor>", "act": "some-agent"} — non-conformant, emitted by us.
Trigger
Requires a subject token whose own act claim is not an object. Our own AS never mints one, so this needs a token from another issuer — plausible as multi-issuer support lands (see the TODO referencing #5989 in multi_issuer_validator.go). The subject token is signature-validated first, so this is an issuer-conformance problem, not an unauthenticated attack.
Impact
Low severity, but it means ToolHive both emits and consumes a spec violation. As of #6046 the consuming side now flags it — auth.ParseDelegationChain sets malformed: true on the audit event — so the symptom is observable in audit logs while the cause sits on the issuance side.
Suggested fix
Either reject the exchange with invalid_grant when the prior act is not a JSON object, or drop the non-conformant prior act (and log at WARN) so the minted chain stays conformant. Rejecting is the more consistent choice, since the adjacent depth violation already returns invalid_grant.
Worth fixing actChainDepth to distinguish "absent" from "malformed" at the same time, rather than returning 0 for both.
Found while reviewing #6046.
Problem
HandleTokenEndpointRequestcopies the subject token's prioractclaim into the newly minted one without checking it is a JSON object:The depth gate does not catch it either, because
actChainDepthreturns0for anything that is not a map:RFC 8693 §4.1 states: "The
actclaim value is a JSON object, and members in the JSON object are claims that identify the actor." So a subject token carrying e.g."act": "some-agent"or an array produces a minted token withact: {"sub": "<actor>", "act": "some-agent"}— non-conformant, emitted by us.Trigger
Requires a subject token whose own
actclaim is not an object. Our own AS never mints one, so this needs a token from another issuer — plausible as multi-issuer support lands (see the TODO referencing #5989 inmulti_issuer_validator.go). The subject token is signature-validated first, so this is an issuer-conformance problem, not an unauthenticated attack.Impact
Low severity, but it means ToolHive both emits and consumes a spec violation. As of #6046 the consuming side now flags it —
auth.ParseDelegationChainsetsmalformed: trueon the audit event — so the symptom is observable in audit logs while the cause sits on the issuance side.Suggested fix
Either reject the exchange with
invalid_grantwhen the prioractis not a JSON object, or drop the non-conformant prioract(and log at WARN) so the minted chain stays conformant. Rejecting is the more consistent choice, since the adjacent depth violation already returnsinvalid_grant.Worth fixing
actChainDepthto distinguish "absent" from "malformed" at the same time, rather than returning0for both.