Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 11 additions & 8 deletions .github/workflows/govulncheck.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: ./...
6 changes: 4 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
)
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
54 changes: 54 additions & 0 deletions validator/security.go
Original file line number Diff line number Diff line change
@@ -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
}
136 changes: 136 additions & 0 deletions validator/security_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}
7 changes: 7 additions & 0 deletions validator/validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion validator/validator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
},
Expand Down
Loading