security: GHSA-g962-2j28-3cg9 (HIGH) JWT Audience Validation Bypass in Local Key and HMAC Modes

This commit is contained in:
jamesread 2026-03-03 22:47:03 +00:00
parent 92a1346edf
commit e97d8ecbd8
2 changed files with 40 additions and 6 deletions

View File

@ -33,6 +33,13 @@ func parseJwtToken(cfg *config.Config, jwtString string) (*jwt.Token, error) {
return parseJwtTokenWithHMAC(cfg, jwtString) return parseJwtTokenWithHMAC(cfg, jwtString)
} }
func parserOptionsWithAudience(cfg *config.Config) []jwt.ParserOption {
if cfg.AuthJwtAud == "" {
return nil
}
return []jwt.ParserOption{jwt.WithAudience(cfg.AuthJwtAud)}
}
func getClaimsFromJwtToken(cfg *config.Config, jwtString string) (jwt.MapClaims, error) { func getClaimsFromJwtToken(cfg *config.Config, jwtString string) (jwt.MapClaims, error) {
token, err := parseJwtToken(cfg, jwtString) token, err := parseJwtToken(cfg, jwtString)
@ -56,7 +63,8 @@ func parseJwtTokenWithRemoteKey(cfg *config.Config, jwtToken string) (*jwt.Token
return nil, err return nil, err
} }
return jwt.Parse(jwtToken, jwksVerifier.Keyfunc, jwt.WithAudience(cfg.AuthJwtAud)) opts := parserOptionsWithAudience(cfg)
return jwt.Parse(jwtToken, jwksVerifier.Keyfunc, opts...)
} }
var ( var (
@ -148,24 +156,30 @@ func parseJwtTokenWithLocalKey(cfg *config.Config, jwtString string) (*jwt.Token
return nil, err return nil, err
} }
return jwt.Parse(jwtString, func(token *jwt.Token) (interface{}, error) { keyFunc := func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok { if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
return nil, fmt.Errorf("parseJwt expected token algorithm RSA but got: %v", token.Header["alg"]) return nil, fmt.Errorf("parseJwt expected token algorithm RSA but got: %v", token.Header["alg"])
} }
return pubKey, nil return pubKey, nil
}) }
opts := parserOptionsWithAudience(cfg)
return jwt.Parse(jwtString, keyFunc, opts...)
} }
// Hash-based Message Authentication Code // Hash-based Message Authentication Code
func parseJwtTokenWithHMAC(cfg *config.Config, jwtString string) (*jwt.Token, error) { func parseJwtTokenWithHMAC(cfg *config.Config, jwtString string) (*jwt.Token, error) {
return jwt.Parse(jwtString, func(token *jwt.Token) (interface{}, error) { keyFunc := func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("parseJwt expected token algorithm HMAC but got: %v", token.Header["alg"]) return nil, fmt.Errorf("parseJwt expected token algorithm HMAC but got: %v", token.Header["alg"])
} }
return []byte(cfg.AuthJwtHmacSecret), nil return []byte(cfg.AuthJwtHmacSecret), nil
}) }
opts := parserOptionsWithAudience(cfg)
return jwt.Parse(jwtString, keyFunc, opts...)
} }
func lookupClaimValueOrDefault(claims jwt.MapClaims, key string, def string) string { func lookupClaimValueOrDefault(claims jwt.MapClaims, key string, def string) string {

View File

@ -66,12 +66,19 @@ func newMux() *http.ServeMux {
} }
func createJWTTokenWithExpiration(t *testing.T, privateKey *rsa.PrivateKey, expire int64) string { func createJWTTokenWithExpiration(t *testing.T, privateKey *rsa.PrivateKey, expire int64) string {
return createJWTTokenWithExpirationAndAudience(t, privateKey, expire, "")
}
func createJWTTokenWithExpirationAndAudience(t *testing.T, privateKey *rsa.PrivateKey, expire int64, audience string) string {
token := jwt.New(jwt.SigningMethodRS256) token := jwt.New(jwt.SigningMethodRS256)
claims := token.Claims.(jwt.MapClaims) claims := token.Claims.(jwt.MapClaims)
claims["nbf"] = time.Now().Unix() - 1000 claims["nbf"] = time.Now().Unix() - 1000
claims["exp"] = time.Now().Unix() + expire claims["exp"] = time.Now().Unix() + expire
claims["sub"] = "test" claims["sub"] = "test"
claims["olivetinGroup"] = "test" claims["olivetinGroup"] = "test"
if audience != "" {
claims["aud"] = audience
}
tokenStr, err := token.SignedString(privateKey) tokenStr, err := token.SignedString(privateKey)
if err != nil { if err != nil {
@ -108,6 +115,10 @@ func verifyJWTResponse(t *testing.T, res *http.Response, expectCode int) {
} }
func testJwkValidation(t *testing.T, expire int64, expectCode int) { func testJwkValidation(t *testing.T, expire int64, expectCode int) {
testJwkValidationWithAudience(t, expire, expectCode, "", "")
}
func testJwkValidationWithAudience(t *testing.T, expire int64, expectCode int, configAudience, tokenAudience string) {
privateKey, publicKeyPath := createKeys(t) privateKey, publicKeyPath := createKeys(t)
defer os.Remove(publicKeyPath) defer os.Remove(publicKeyPath)
@ -116,8 +127,9 @@ func testJwkValidation(t *testing.T, expire int64, expectCode int) {
cfg.AuthJwtClaimUsername = "sub" cfg.AuthJwtClaimUsername = "sub"
cfg.AuthJwtClaimUserGroup = "olivetinGroup" cfg.AuthJwtClaimUserGroup = "olivetinGroup"
cfg.AuthJwtHeader = "Authorization" cfg.AuthJwtHeader = "Authorization"
cfg.AuthJwtAud = configAudience
tokenStr := createJWTTokenWithExpiration(t, privateKey, expire) tokenStr := createJWTTokenWithExpirationAndAudience(t, privateKey, expire, tokenAudience)
handler := setupJWTTestHandler(t, cfg) handler := setupJWTTestHandler(t, cfg)
srv := httptest.NewServer(handler) srv := httptest.NewServer(handler)
@ -135,6 +147,14 @@ func TestJWTSignatureVerificationFails(t *testing.T) {
testJwkValidation(t, -500, 403) testJwkValidation(t, -500, 403)
} }
func TestJWTAudienceValidationRejectsWrongAudience(t *testing.T) {
testJwkValidationWithAudience(t, 1000, 403, "expected-audience", "wrong-audience")
}
func TestJWTAudienceValidationAcceptsCorrectAudience(t *testing.T) {
testJwkValidationWithAudience(t, 1000, 200, "expected-audience", "expected-audience")
}
func createJWTTokenWithGroups(t *testing.T, privateKey *rsa.PrivateKey, groups interface{}) string { func createJWTTokenWithGroups(t *testing.T, privateKey *rsa.PrivateKey, groups interface{}) string {
token := jwt.New(jwt.SigningMethodRS256) token := jwt.New(jwt.SigningMethodRS256)
claims := token.Claims.(jwt.MapClaims) claims := token.Claims.(jwt.MapClaims)