From 8171352c43599e5eb56c576e91d479b55414ead6 Mon Sep 17 00:00:00 2001 From: Talos Bot Date: Thu, 30 Jul 2026 18:38:03 +0000 Subject: [PATCH] =?UTF-8?q?Fix:=20=F0=9F=8E=AF=20Reject=20JWTs=20with=20Ou?= =?UTF-8?q?t-of-Bounds=20Expiration=20(`exp`)=20Values=20to=20Prevent=20`t?= =?UTF-8?q?ime.Time`=20Overflow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolves #1 Generated by Talos autonomous bounty hunter. Bounty platform: github Bounty ID: 1 Quality gates passed: - meaningful: ✓ - syntax: ✓ - duplicate: ✓ - title: ✓ - tests: ✓ --- map_claims.go | 166 ++++++++++++++++++++++++++++ map_claims_test.go | 270 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 436 insertions(+) create mode 100644 map_claims.go create mode 100644 map_claims_test.go diff --git a/map_claims.go b/map_claims.go new file mode 100644 index 0000000..244a593 --- /dev/null +++ b/map_claims.go @@ -0,0 +1,166 @@ +package jwt + +import ( + "encoding/json" + "errors" + "time" +) + +var ( + ErrInvalidTimeValue = errors.New("token has invalid time value") +) + +const ( + minUnixTime = -62135596800 + maxUnixTime = 253402300799 +) + +type MapClaims map[string]interface{} + +func (m MapClaims) VerifyAudience(cmp string, req bool) bool { + var aud []string + switch v := m["aud"].(type) { + case string: + aud = []string{v} + case []string: + aud = v + case []interface{}: + for _, a := range v { + vs, ok := a.(string) + if !ok { + return false + } + aud = append(aud, vs) + } + default: + return false + } + return verifyAud(aud, cmp, req) +} + +func (m MapClaims) VerifyExpiresAt(cmp int64, req bool) bool { + exp, ok := m["exp"] + if !ok { + return !req + } + + expTime, err := parseTimeValue(exp) + if err != nil { + return false + } + + return verifyExp(expTime, time.Unix(cmp, 0), req) +} + +func (m MapClaims) VerifyIssuedAt(cmp int64, req bool) bool { + iat, ok := m["iat"] + if !ok { + return !req + } + + iatTime, err := parseTimeValue(iat) + if err != nil { + return false + } + + return verifyIat(iatTime, time.Unix(cmp, 0), req) +} + +func (m MapClaims) VerifyNotBefore(cmp int64, req bool) bool { + nbf, ok := m["nbf"] + if !ok { + return !req + } + + nbfTime, err := parseTimeValue(nbf) + if err != nil { + return false + } + + return verifyNbf(nbfTime, time.Unix(cmp, 0), req) +} + +func (m MapClaims) VerifyIssuer(cmp string, req bool) bool { + iss, _ := m["iss"].(string) + return verifyIss(iss, cmp, req) +} + +func (m MapClaims) Valid() error { + vErr := new(ValidationError) + now := TimeFunc().Unix() + + if exp, ok := m["exp"]; ok { + expTime, err := parseTimeValue(exp) + if err != nil { + vErr.Inner = ErrInvalidTimeValue + vErr.Errors |= ValidationErrorMalformed + return vErr + } + if !verifyExp(expTime, TimeFunc(), false) { + vErr.Inner = errors.New("token is expired") + vErr.Errors |= ValidationErrorExpired + } + } + + if nbf, ok := m["nbf"]; ok { + nbfTime, err := parseTimeValue(nbf) + if err != nil { + vErr.Inner = ErrInvalidTimeValue + vErr.Errors |= ValidationErrorMalformed + return vErr + } + if !verifyNbf(nbfTime, TimeFunc(), false) { + vErr.Inner = errors.New("token is not valid yet") + vErr.Errors |= ValidationErrorNotValidYet + } + } + + if iat, ok := m["iat"]; ok { + iatTime, err := parseTimeValue(iat) + if err != nil { + vErr.Inner = ErrInvalidTimeValue + vErr.Errors |= ValidationErrorMalformed + return vErr + } + if !verifyIat(iatTime, time.Unix(now, 0), false) { + vErr.Inner = errors.New("token used before issued") + vErr.Errors |= ValidationErrorIssuedAt + } + } + + if vErr.Errors == 0 { + return nil + } + + return vErr +} + +func parseTimeValue(val interface{}) (time.Time, error) { + var timestamp float64 + + switch v := val.(type) { + case float64: + timestamp = v + case json.Number: + f, err := v.Float64() + if err != nil { + return time.Time{}, ErrInvalidTimeValue + } + timestamp = f + case int64: + timestamp = float64(v) + case int: + timestamp = float64(v) + default: + return time.Time{}, ErrInvalidTimeValue + } + + if timestamp < minUnixTime || timestamp > maxUnixTime { + return time.Time{}, ErrInvalidTimeValue + } + + sec := int64(timestamp) + nsec := int64((timestamp - float64(sec)) * 1e9) + + return time.Unix(sec, nsec), nil +} diff --git a/map_claims_test.go b/map_claims_test.go new file mode 100644 index 0000000..2447009 --- /dev/null +++ b/map_claims_test.go @@ -0,0 +1,270 @@ +package jwt + +import ( + "encoding/json" + "testing" + "time" +) + +func TestMapClaims_VerifyExpiresAt_Overflow(t *testing.T) { + tests := []struct { + name string + expValue interface{} + shoudFail bool + }{ + { + name: "max int64 overflow", + expValue: float64(9223372036854775807), + shoudFail: true, + }, + { + name: "large floating point 1e21", + expValue: 1e21, + shoudFail: true, + }, + { + name: "large floating point 1e20", + expValue: 1e20, + shoudFail: true, + }, + { + name: "beyond year 9999", + expValue: float64(253402300800), + shoudFail: true, + }, + { + name: "valid future date year 2050", + expValue: float64(2524608000), + shoudFail: false, + }, + { + name: "valid near max boundary", + expValue: float64(253402300799), + shoudFail: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + claims := MapClaims{ + "exp": tt.expValue, + } + + now := time.Now().Unix() + result := claims.VerifyExpiresAt(now, true) + + if tt.shoudFail && result { + t.Errorf("Expected verification to fail for %s, but it passed", tt.name) + } + if !tt.shoudFail && !result { + t.Errorf("Expected verification to pass for %s, but it failed", tt.name) + } + }) + } +} + +func TestMapClaims_Valid_Overflow(t *testing.T) { + tests := []struct { + name string + claims MapClaims + expectError bool + errorType uint32 + }{ + { + name: "exp overflow max int64", + claims: MapClaims{ + "exp": float64(9223372036854775807), + }, + expectError: true, + errorType: ValidationErrorMalformed, + }, + { + name: "exp overflow 1e21", + claims: MapClaims{ + "exp": 1e21, + }, + expectError: true, + errorType: ValidationErrorMalformed, + }, + { + name: "nbf overflow", + claims: MapClaims{ + "nbf": 1e20, + }, + expectError: true, + errorType: ValidationErrorMalformed, + }, + { + name: "iat overflow", + claims: MapClaims{ + "iat": float64(253402300800), + }, + expectError: true, + errorType: ValidationErrorMalformed, + }, + { + name: "valid future exp", + claims: MapClaims{ + "exp": float64(time.Now().Add(24 * time.Hour).Unix()), + }, + expectError: false, + }, + { + name: "valid exp at year 9999 boundary", + claims: MapClaims{ + "exp": float64(253402300799), + }, + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.claims.Valid() + + if tt.expectError { + if err == nil { + t.Errorf("Expected error for %s, but got none", tt.name) + return + } + if vErr, ok := err.(*ValidationError); ok { + if vErr.Errors&tt.errorType == 0 { + t.Errorf("Expected error type %d, but got %d", tt.errorType, vErr.Errors) + } + } else { + t.Errorf("Expected ValidationError, but got %T", err) + } + } else { + if err != nil { + t.Errorf("Expected no error for %s, but got: %v", tt.name, err) + } + } + }) + } +} + +func TestMapClaims_VerifyNotBefore_Overflow(t *testing.T) { + tests := []struct { + name string + nbfValue interface{} + shouldFail bool + }{ + { + name: "nbf overflow", + nbfValue: 1e20, + shouldFail: true, + }, + { + name: "valid nbf", + nbfValue: float64(time.Now().Add(-1 * time.Hour).Unix()), + shouldFail: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + claims := MapClaims{ + "nbf": tt.nbfValue, + } + + now := time.Now().Unix() + result := claims.VerifyNotBefore(now, true) + + if tt.shouldFail && result { + t.Errorf("Expected verification to fail for %s, but it passed", tt.name) + } + if !tt.shouldFail && !result { + t.Errorf("Expected verification to pass for %s, but it failed", tt.name) + } + }) + } +} + +func TestMapClaims_VerifyIssuedAt_Overflow(t *testing.T) { + tests := []struct { + name string + iatValue interface{} + shouldFail bool + }{ + { + name: "iat overflow", + iatValue: float64(253402300800), + shouldFail: true, + }, + { + name: "valid iat", + iatValue: float64(time.Now().Add(-1 * time.Hour).Unix()), + shouldFail: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + claims := MapClaims{ + "iat": tt.iatValue, + } + + now := time.Now().Unix() + result := claims.VerifyIssuedAt(now, true) + + if tt.shouldFail && result { + t.Errorf("Expected verification to fail for %s, but it passed", tt.name) + } + if !tt.shouldFail && !result { + t.Errorf("Expected verification to pass for %s, but it failed", tt.name) + } + }) + } +} + +func TestParseTimeValue(t *testing.T) { + tests := []struct { + name string + value interface{} + expectErr bool + }{ + { + name: "valid float64", + value: float64(1609459200), + expectErr: false, + }, + { + name: "valid json.Number", + value: json.Number("1609459200"), + expectErr: false, + }, + { + name: "overflow max boundary", + value: float64(253402300800), + expectErr: true, + }, + { + name: "overflow 1e21", + value: 1e21, + expectErr: true, + }, + { + name: "invalid type string", + value: "not a number", + expectErr: true, + }, + { + name: "at max boundary", + value: float64(253402300799), + expectErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := parseTimeValue(tt.value) + + if tt.expectErr && err == nil { + t.Errorf("Expected error for %s, but got none", tt.name) + } + if !tt.expectErr && err != nil { + t.Errorf("Expected no error for %s, but got: %v", tt.name, err) + } + }) + } +}