diff --git a/.github/workflows/govulncheck.yml b/.github/workflows/govulncheck.yml index 6247bf1a..d7d67aca 100644 --- a/.github/workflows/govulncheck.yml +++ b/.github/workflows/govulncheck.yml @@ -17,12 +17,15 @@ jobs: uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.head.sha || github.ref }} - - name: Read Go version from go.mod - id: go-version - run: echo "GO_VERSION=$(grep '^go ' go.mod | cut -d ' ' -f 2)" >> $GITHUB_ENV - - name: Scan for Vulnerabilities in Code - uses: Templum/govulncheck-action@0eeca9d81f01facc00829cc99a14e44ce59ce80f # pin@1.0.2 + + - name: Setup Go + uses: actions/setup-go@v5 with: - go-version: ${{ env.GO_VERSION }} - package: ./... - fail-on-vuln: true + go-version-file: go.mod + cache: true + + - name: Run govulncheck + uses: golang/govulncheck-action@b625fbe08f3bccbe446d94fbf87fcc875a4f50ee # pin@1.0.4 + with: + go-version-file: go.mod + go-package: ./... diff --git a/go.mod b/go.mod index 0b61c02d..5c11640b 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,8 @@ module github.com/auth0/go-jwt-middleware/v2 -go 1.23.0 +go 1.24.0 + +toolchain go1.24.8 require ( github.com/google/go-cmp v0.7.0 @@ -12,6 +14,6 @@ require ( require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/crypto v0.35.0 // indirect + golang.org/x/crypto v0.44.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 044dd052..30d68105 100644 --- a/go.sum +++ b/go.sum @@ -6,8 +6,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= -golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= +golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU= +golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc= golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= diff --git a/validator/security.go b/validator/security.go new file mode 100644 index 00000000..8eebcaa9 --- /dev/null +++ b/validator/security.go @@ -0,0 +1,54 @@ +package validator + +import ( + "errors" + "strings" +) + +var ( + // ErrExcessiveTokenDots is returned when a token contains too many dots, + // which could indicate a malicious attempt to exploit CVE-2025-27144. + ErrExcessiveTokenDots = errors.New("token contains excessive dots (possible DoS attack)") +) + +const ( + // maxTokenDots is the maximum number of dots allowed in a JWT token. + // Valid formats: + // - JWS compact: header.payload.signature (2 dots) + // - JWE compact: header.key.iv.ciphertext.tag (4 dots) + // - JWE with multiple recipients: can have more sections + // We allow up to 5 dots to be safe, which covers all valid use cases. + maxTokenDots = 5 +) + +// validateTokenFormat performs pre-validation on the token string to protect +// against CVE-2025-27144 (memory exhaustion via excessive dots). +// +// This is a defense-in-depth measure for v2.x which uses go-jose v2. +// The underlying vulnerability is in go-jose v2's use of strings.Split() +// without limits. This function rejects obviously malicious inputs before +// they reach the vulnerable code. +// +// Note: This is a workaround, not a complete fix. The vulnerability is +// fully resolved in v3.x which uses lestrrat-go/jwx. +func validateTokenFormat(tokenString string) error { + // Count dots in the token + dotCount := strings.Count(tokenString, ".") + + if dotCount > maxTokenDots { + return ErrExcessiveTokenDots + } + + // Additional basic validation + if len(tokenString) == 0 { + return errors.New("token is empty") + } + + // Reject tokens that are suspiciously large (> 1MB) + // Valid JWTs should rarely exceed a few KB + if len(tokenString) > 1024*1024 { + return errors.New("token exceeds maximum size (1MB)") + } + + return nil +} diff --git a/validator/security_test.go b/validator/security_test.go new file mode 100644 index 00000000..482c1a98 --- /dev/null +++ b/validator/security_test.go @@ -0,0 +1,136 @@ +package validator + +import ( + "context" + "errors" + "strings" + "testing" +) + +func TestValidateTokenFormat(t *testing.T) { + tests := []struct { + name string + token string + expectErr error + }{ + { + name: "valid JWS token (2 dots)", + token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.signature", + expectErr: nil, + }, + { + name: "valid JWE token (4 dots)", + token: "header.encrypted_key.iv.ciphertext.tag", + expectErr: nil, + }, + { + name: "max allowed dots (5)", + token: "a.b.c.d.e.f", + expectErr: nil, + }, + { + name: "excessive dots (6) - CVE-2025-27144", + token: "a.b.c.d.e.f.g", + expectErr: ErrExcessiveTokenDots, + }, + { + name: "many dots (100) - CVE-2025-27144", + token: strings.Repeat("a.", 100) + "z", + expectErr: ErrExcessiveTokenDots, + }, + { + name: "malicious token with 10000 dots", + token: strings.Repeat(".", 10000), + expectErr: ErrExcessiveTokenDots, + }, + { + name: "empty token", + token: "", + expectErr: errors.New("token is empty"), + }, + { + name: "token exceeds 1MB", + token: strings.Repeat("a", 1024*1024+1), + expectErr: errors.New("token exceeds maximum size (1MB)"), + }, + { + name: "token exactly 1MB (allowed)", + token: "header." + strings.Repeat("a", 1024*1024-20) + ".sig", + expectErr: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateTokenFormat(tt.token) + + if tt.expectErr == nil { + if err != nil { + t.Errorf("expected no error, got: %v", err) + } + } else { + if err == nil { + t.Errorf("expected error containing '%v', got nil", tt.expectErr) + } else if !errors.Is(err, tt.expectErr) && !strings.Contains(err.Error(), tt.expectErr.Error()) { + t.Errorf("expected error '%v', got '%v'", tt.expectErr, err) + } + } + }) + } +} + +func TestValidateToken_CVE_2025_27144_Protection(t *testing.T) { + // This test ensures the CVE-2025-27144 mitigation is in place + v, err := New( + func(_ context.Context) (interface{}, error) { + return []byte("secret"), nil + }, + HS256, + "https://issuer.example.com/", + []string{"audience"}, + ) + if err != nil { + t.Fatalf("failed to create validator: %v", err) + } + + // Test with malicious token containing excessive dots + maliciousToken := strings.Repeat("a.", 1000) + "z" + + _, err = v.ValidateToken(context.Background(), maliciousToken) + + if err == nil { + t.Error("expected error for malicious token, got nil") + } + + if !errors.Is(err, ErrExcessiveTokenDots) && !strings.Contains(err.Error(), "excessive dots") { + t.Errorf("expected error about excessive dots, got: %v", err) + } +} + +func BenchmarkValidateTokenFormat(b *testing.B) { + tests := []struct { + name string + token string + }{ + { + name: "normal token", + token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.signature", + }, + { + name: "malicious 100 dots", + token: strings.Repeat("a.", 100) + "z", + }, + { + name: "malicious 1000 dots", + token: strings.Repeat("a.", 1000) + "z", + }, + } + + for _, tt := range tests { + b.Run(tt.name, func(b *testing.B) { + for i := 0; i < b.N; i++ { + _ = validateTokenFormat(tt.token) + } + }) + } +} diff --git a/validator/validator.go b/validator/validator.go index 2a302493..8fa71193 100644 --- a/validator/validator.go +++ b/validator/validator.go @@ -94,6 +94,13 @@ func New( // ValidateToken validates the passed in JWT using the jose v2 package. func (v *Validator) ValidateToken(ctx context.Context, tokenString string) (interface{}, error) { + // CVE-2025-27144 mitigation: Validate token format before parsing + // to prevent memory exhaustion from malicious tokens with excessive dots. + // This is a defense-in-depth measure for v2.x. + if err := validateTokenFormat(tokenString); err != nil { + return nil, fmt.Errorf("invalid token format: %w", err) + } + token, err := jwt.ParseSigned(tokenString) if err != nil { return nil, fmt.Errorf("could not parse the token: %w", err) diff --git a/validator/validator_test.go b/validator/validator_test.go index 08feeb14..c97d6f07 100644 --- a/validator/validator_test.go +++ b/validator/validator_test.go @@ -84,7 +84,7 @@ func TestValidator_ValidateToken(t *testing.T) { }, { name: "it throws an error when it cannot parse the token", - token: "", + token: "a.b", keyFunc: func(context.Context) (interface{}, error) { return []byte("secret"), nil },