From 8a9449df635bbc46ba75ef8bacdd9baaab9db2c9 Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Mon, 21 Sep 2026 19:43:35 +0200 Subject: [PATCH] fix(06-12): reject typed-nil guards and fractional JWT subjects --- bouncer/jwt.go | 9 +++++++-- bouncer/jwt_test.go | 12 ++++++++++++ bouncer/registry.go | 7 +++++++ bouncer/registry_test.go | 12 ++++++++++++ 4 files changed, 38 insertions(+), 2 deletions(-) diff --git a/bouncer/jwt.go b/bouncer/jwt.go index 9643ffa..2feb517 100644 --- a/bouncer/jwt.go +++ b/bouncer/jwt.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "math" "net/http" "strconv" "strings" @@ -148,12 +149,16 @@ func subject(claims jwt.MapClaims) string { case string: return strings.TrimSpace(v) case float64: - if v <= 0 { + if math.IsNaN(v) || math.IsInf(v, 0) || v <= 0 || v != math.Trunc(v) || v > 9007199254740992 { return "" } return strconv.FormatInt(int64(v), 10) case json.Number: - return strings.TrimSpace(v.String()) + n, err := strconv.ParseInt(strings.TrimSpace(v.String()), 10, 64) + if err != nil || n <= 0 { + return "" + } + return strconv.FormatInt(n, 10) default: return "" } diff --git a/bouncer/jwt_test.go b/bouncer/jwt_test.go index 16c2e0a..c293ce2 100644 --- a/bouncer/jwt_test.go +++ b/bouncer/jwt_test.go @@ -240,3 +240,15 @@ func noneToken(t *testing.T, claims jwt.MapClaims) string { body := base64.RawURLEncoding.EncodeToString(payload) return header + "." + body + "." } + +func TestVerifyRejectsFractionalSubject(t *testing.T) { + exp := time.Now().Add(time.Hour).Unix() + tok := sign(t, jwt.SigningMethodHS256, jwt.MapClaims{"sub": 12.5, "exp": exp}, []byte(secret)) + if sub, err := Verify(tok, secret); err == nil || sub != "" { + t.Fatalf("fractional sub accepted: %q %v", sub, err) + } + tok = sign(t, jwt.SigningMethodHS256, jwt.MapClaims{"sub": 12, "exp": exp}, []byte(secret)) + if sub, err := Verify(tok, secret); err != nil || sub != "12" { + t.Fatalf("whole sub: %q %v", sub, err) + } +} diff --git a/bouncer/registry.go b/bouncer/registry.go index f5c0182..116b5a7 100644 --- a/bouncer/registry.go +++ b/bouncer/registry.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "net/http" + "reflect" ) type namedGuard struct { @@ -32,6 +33,12 @@ func (reg *Registry) Register(pluginID, name string, g any) error { if name == "" || g == nil { return fmt.Errorf("bouncer: plugin %q registered empty guard %q", pluginID, name) } + switch rv := reflect.ValueOf(g); rv.Kind() { + case reflect.Pointer, reflect.Map, reflect.Slice, reflect.Func, reflect.Chan, reflect.Interface: + if rv.IsNil() { + return fmt.Errorf("bouncer: plugin %q registered empty guard %q", pluginID, name) + } + } _, isGuard := g.(Guard) _, isCred := g.(CredentialGuard) if !isGuard && !isCred { diff --git a/bouncer/registry_test.go b/bouncer/registry_test.go index 8f27daf..2e5494e 100644 --- a/bouncer/registry_test.go +++ b/bouncer/registry_test.go @@ -233,3 +233,15 @@ func TestJWTGuardRegistryMatchesMiddleware(t *testing.T) { }) } } + +func TestRegisterRejectsTypedNilGuard(t *testing.T) { + var reg Registry + var g *credPtrGuard + if err := reg.Register("p", "n", g); err == nil { + t.Fatal("typed-nil guard accepted") + } +} + +type credPtrGuard struct{} + +func (*credPtrGuard) Authenticate(*http.Request) (*Principal, error) { return nil, nil }