Skip to content
Open
Changes from 1 commit
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
227 changes: 227 additions & 0 deletions fuzz_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
//go:build go1.18
// +build go1.18

// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
package hydra_test

import (
"context"
"crypto/rand"
"crypto/rsa"
"crypto/sha512"
"hash"
"testing"

tokenhmac "github.com/ory/hydra/v2/fosite/token/hmac"
fositejwt "github.com/ory/hydra/v2/fosite/token/jwt"
)

// testConfig satisfies all HMAC strategy config interfaces.
type testConfig struct {
secret []byte
}

func (c *testConfig) GetTokenEntropy(_ context.Context) int { return 32 }
func (c *testConfig) GetGlobalSecret(_ context.Context) ([]byte, error) { return c.secret, nil }
func (c *testConfig) GetRotatedGlobalSecrets(_ context.Context) ([][]byte, error) {
return nil, nil
}
func (c *testConfig) GetHMACHasher(_ context.Context) func() hash.Hash {
return sha512.New512_256
}

func mustMakeSecret(tb testing.TB) []byte {
tb.Helper()
secret := make([]byte, 32)
if _, err := rand.Read(secret); err != nil {
tb.Fatal(err)
}
return secret
}

// FuzzHMACTokenValidate tests HMAC token validation with arbitrary
// attacker-controlled token strings. This is the pre-auth boundary
// for every OAuth2 access token, refresh token, and authorization
// code in Hydra. The token comes from the Authorization: Bearer header.
func FuzzHMACTokenValidate(f *testing.F) {
secret := mustMakeSecret(f)
cfg := &testConfig{secret: secret}
strategy := &tokenhmac.HMACStrategy{Config: cfg}

// Generate a valid token for the seed corpus
validToken, _, err := strategy.Generate(context.Background())
if err == nil {
f.Add(validToken)
}

f.Add("") // empty
f.Add(".") // just separator
f.Add("abc.def") // invalid base64
f.Add("AAAA.AAAA") // valid base64, wrong signature
f.Add("not-a-token") // no separator
f.Add(string(make([]byte, 10000))) // large input

f.Fuzz(func(t *testing.T, token string) {
if len(token) > 1<<16 {
return
}
// Validate should never panic, only return errors
_ = strategy.Validate(context.Background(), token)
})
}

// FuzzHMACTokenSignature tests the Signature extraction method
// with arbitrary token strings. Signature is used to look up
// tokens in storage — a crash here is a DoS vector.
func FuzzHMACTokenSignature(f *testing.F) {
secret := mustMakeSecret(f)
cfg := &testConfig{secret: secret}
strategy := &tokenhmac.HMACStrategy{Config: cfg}

// Create a real token for valid case
validToken, _, err := strategy.Generate(context.Background())
if err == nil {
f.Add(validToken, validToken)
}

f.Fuzz(func(t *testing.T, token, compare string) {
_ = strategy.Signature(token)
})
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

// FuzzHMACGenerateForString tests HMAC generation for arbitrary
// string inputs. Used to hash client secrets and other sensitive
// strings — a crash here can break secret hashing.
func FuzzHMACGenerateForString(f *testing.F) {
secret := mustMakeSecret(f)
cfg := &testConfig{secret: secret}
strategy := &tokenhmac.HMACStrategy{Config: cfg}

f.Add("test-string")
f.Add("")
f.Add(string(make([]byte, 10000)))

f.Fuzz(func(t *testing.T, text string) {
_, _ = strategy.GenerateHMACForString(context.Background(), text)
})
Comment on lines +106 to +108

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add input size cap for consistency and DoS prevention.

Unlike the other fuzz functions in this file, FuzzHMACGenerateForString does not cap the input size. This could allow the fuzzer to generate very large strings that exhaust memory or slow down fuzzing.

🛡️ Proposed fix to add size cap
 	f.Fuzz(func(t *testing.T, text string) {
+		if len(text) > 1<<16 {
+			return
+		}
 		_, _ = strategy.GenerateHMACForString(context.Background(), text)
 	})
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
f.Fuzz(func(t *testing.T, text string) {
_, _ = strategy.GenerateHMACForString(context.Background(), text)
})
f.Fuzz(func(t *testing.T, text string) {
if len(text) > 1<<16 {
return
}
_, _ = strategy.GenerateHMACForString(context.Background(), text)
})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@fuzz_test.go` around lines 117 - 119, The fuzz harness for
GenerateHMACForString lacks an input-size cap and can be DoS'ed by huge strings;
modify the Fuzz function wrapping strategy.GenerateHMACForString to early-return
or t.Skip when the fuzzed string exceeds a safe length (e.g., 1KB–4KB) by
checking len(text) and skipping/returning before calling
strategy.GenerateHMACForString, so the test stays consistent with other fuzz
functions and avoids memory exhaustion.

}

// FuzzJWTValidate tests JWT token validation with arbitrary
// attacker-controlled JWT strings. This is the pre-auth boundary
// for JWT access tokens and OpenID Connect ID tokens.
func FuzzJWTValidate(f *testing.F) {
// Generate a test RSA key
key, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
f.Fatal(err)
}

signer := &fositejwt.DefaultSigner{
GetPrivateKey: func(_ context.Context) (interface{}, error) {
return key, nil
},
}

// Generate valid token as seed
claims := fositejwt.MapClaims{
"sub": "test-user",
"iss": "https://hydra.example.com",
}
validToken, _, err := signer.Generate(context.Background(), claims, &fositejwt.Headers{})
if err == nil {
f.Add(validToken)
}

f.Add("") // empty
f.Add("eyJ...") // garbage
f.Add("a.b.c") // 3-part but invalid
f.Add("header.payload") // 2-part

f.Fuzz(func(t *testing.T, token string) {
if len(token) > 1<<16 {
return
}
// Validate should never panic
_, _ = signer.Validate(context.Background(), token)
})
}

// FuzzJWTDecode tests JWT token decoding with arbitrary token strings.
// Decode is used even before Validate in many code paths.
func FuzzJWTDecode(f *testing.F) {
key, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
f.Fatal(err)
}

signer := &fositejwt.DefaultSigner{
GetPrivateKey: func(_ context.Context) (interface{}, error) {
return key, nil
},
}

claims := fositejwt.MapClaims{"sub": "test"}
validToken, _, err := signer.Generate(context.Background(), claims, &fositejwt.Headers{})
if err == nil {
f.Add(validToken)
}

f.Add("")
f.Add("...")
f.Add(string(make([]byte, 100000)))

f.Fuzz(func(t *testing.T, token string) {
if len(token) > 1<<16 {
return
}
Comment on lines +173 to +178

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Seed size exceeds fuzz cap.

The seed on line 184 adds a 100,000-byte string, but the fuzz function caps inputs at 64KB (1<<16 = 65,536 bytes). The seed will be immediately discarded by the size check.

♻️ Proposed fix to align seed with cap
-	f.Add(string(make([]byte, 100000)))
+	f.Add(string(make([]byte, 1<<16)))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
f.Add(string(make([]byte, 100000)))
f.Fuzz(func(t *testing.T, token string) {
if len(token) > 1<<16 {
return
}
f.Add(string(make([]byte, 1<<16)))
f.Fuzz(func(t *testing.T, token string) {
if len(token) > 1<<16 {
return
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@fuzz_test.go` around lines 184 - 189, The seed added via
f.Add(string(make([]byte, 100000))) exceeds the fuzz input cap checked in the
Fuzz callback (len(token) > 1<<16), so it will be discarded; change the seed to
be at or below the cap (<= 1<<16 bytes) or remove it entirely. Locate the f.Add
call and replace the 100000-byte seed with a smaller seed (e.g., make([]byte,
1<<16) or a smaller realistic input) so it passes the size check in the
Fuzz(func(t *testing.T, token string)) block.

// Decode should never panic
_, _ = signer.Decode(context.Background(), token)
})
}

// FuzzJWTGetSignature tests JWT signature extraction with arbitrary
// token strings. The signature is used to identify tokens for revocation.
func FuzzJWTGetSignature(f *testing.F) {
key, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
f.Fatal(err)
}

signer := &fositejwt.DefaultSigner{
GetPrivateKey: func(_ context.Context) (interface{}, error) {
return key, nil
},
}

validToken, _, err := signer.Generate(
context.Background(),
fositejwt.MapClaims{"sub": "test"},
&fositejwt.Headers{},
)
if err == nil {
f.Add(validToken)
}

f.Fuzz(func(t *testing.T, token string) {
if len(token) > 1<<16 {
return
}
_, _ = signer.GetSignature(context.Background(), token)
})
}

// Ensure deterministic per-fuzz, not per-package
var _ = tokenhmac.RandomBytes
Loading