-
Notifications
You must be signed in to change notification settings - Fork 1.6k
test: add Go fuzz tests for OSS-Fuzz integration #4106
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 1 commit
a3fbddd
e543e18
e4fec49
87d4f55
dad28e9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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. | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| 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) | ||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||
|
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add input size cap for consistency and DoS prevention. Unlike the other fuzz functions in this file, 🛡️ 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||
| // 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 | ||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.