Context
Filed for internal follow-up from an external security researcher report. Tracked here rather than via the bug bounty program, since obol-stack is out of the program's scope.
The SIWX one-shot EIP-4361 verification correctly binds the signed message to the request host (Authenticate / expectedDomain in internal/x402/siwx.go). However, the reusable session token minted afterwards does not carry that binding:
MintSession (internal/x402/siwx.go) signs siwxSessionClaims{ Wallet, Exp } with the per-process HMAC secret. There is no host, origin, or offer claim.
VerifySession checks only the HMAC signature and expiry, then returns the wallet.
Because a single verifier instance serves multiple routes/offers and can front multiple origins (see the shared-origin vs dedicated-hostname handling in internal/x402/authgate.go), a session token minted after signing in to one offer is accepted on another offer served by the same verifier, after which X-Verified-Wallet is injected for that wallet on the second offer. In a deployment where offers have differing trust levels, this permits cross-offer reuse of a session token (an audience-confusion weakness).
Scope note on blast radius: the session secret is generated per verifier process (NewSIWXAuthenticator -> crypto/rand), so a token never crosses separate verifier instances. The concern is confined to a single verifier instance that fronts more than one offer/origin.
Changes
- Add an origin/offer (audience) claim to
siwxSessionClaims in internal/x402/siwx.go.
- Populate it at
MintSession time from the verified request host/offer.
- Enforce it in
VerifySession, rejecting a token presented on an origin/offer other than the one it was minted for.
- Add a regression test: a token minted for offer A is rejected when presented on offer B.
Context
Filed for internal follow-up from an external security researcher report. Tracked here rather than via the bug bounty program, since obol-stack is out of the program's scope.
The SIWX one-shot EIP-4361 verification correctly binds the signed message to the request host (
Authenticate/expectedDomainininternal/x402/siwx.go). However, the reusable session token minted afterwards does not carry that binding:MintSession(internal/x402/siwx.go) signssiwxSessionClaims{ Wallet, Exp }with the per-process HMAC secret. There is no host, origin, or offer claim.VerifySessionchecks only the HMAC signature and expiry, then returns the wallet.Because a single verifier instance serves multiple routes/offers and can front multiple origins (see the shared-origin vs dedicated-hostname handling in
internal/x402/authgate.go), a session token minted after signing in to one offer is accepted on another offer served by the same verifier, after whichX-Verified-Walletis injected for that wallet on the second offer. In a deployment where offers have differing trust levels, this permits cross-offer reuse of a session token (an audience-confusion weakness).Scope note on blast radius: the session secret is generated per verifier process (
NewSIWXAuthenticator->crypto/rand), so a token never crosses separate verifier instances. The concern is confined to a single verifier instance that fronts more than one offer/origin.Changes
siwxSessionClaimsininternal/x402/siwx.go.MintSessiontime from the verified request host/offer.VerifySession, rejecting a token presented on an origin/offer other than the one it was minted for.