forked from graph-gophers/graphql-go
-
Notifications
You must be signed in to change notification settings - Fork 0
Add MaxSelectionSetSize option to prevent DDoS attacks #10
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
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| name: Tests | ||
|
|
||
| on: | ||
| push: | ||
| branches: [ master ] | ||
| pull_request: | ||
| branches: [ master ] | ||
|
|
||
| jobs: | ||
| test: | ||
| name: Test | ||
| runs-on: ubuntu-latest | ||
| permissions: | ||
| contents: read | ||
| steps: | ||
| - name: Check out code | ||
| uses: actions/checkout@main | ||
|
|
||
| - name: Set up Go | ||
| uses: actions/setup-go@main | ||
| with: | ||
| go-version-file: go.mod | ||
|
|
||
| - name: Run tests | ||
| run: go test -v -race ./... | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,217 @@ | ||
| package graphql_test | ||
|
|
||
| import ( | ||
| "context" | ||
| "strings" | ||
| "testing" | ||
| "time" | ||
|
|
||
| "github.com/graph-gophers/graphql-go" | ||
| ) | ||
|
|
||
| const simpleSchema = ` | ||
| schema { | ||
| query: Query | ||
| } | ||
|
|
||
| type Query { | ||
| a: String | ||
| } | ||
| ` | ||
|
|
||
| type simpleResolver struct{} | ||
|
|
||
| func (r *simpleResolver) A() *string { | ||
| val := "value" | ||
| return &val | ||
| } | ||
|
|
||
| // TestDDoSVulnerability_ManyFieldsAtSameLevel tests the vulnerability where | ||
| // a query with thousands of fields at the same level causes CPU overload. | ||
| // This test demonstrates the vulnerability and is skipped by default. | ||
| func TestDDoSVulnerability_ManyFieldsAtSameLevel(t *testing.T) { | ||
| t.Skip("Skipping vulnerability demonstration test - it would timeout without the fix") | ||
| // Create a query with many duplicate fields at the same level | ||
| // This is the attack vector from the user's report | ||
| numFields := 5000 | ||
| fields := make([]string, numFields) | ||
| for i := 0; i < numFields; i++ { | ||
| fields[i] = "a" | ||
| } | ||
|
|
||
| maliciousQuery := "query { " + strings.Join(fields, " ") + " }" | ||
|
|
||
| schema := graphql.MustParseSchema(simpleSchema, &simpleResolver{}) | ||
|
|
||
| // Set a timeout to prevent the test from hanging indefinitely | ||
| ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) | ||
| defer cancel() | ||
|
|
||
| // This should complete quickly, but without a fix it will cause CPU overload | ||
| done := make(chan struct{}) | ||
| go func() { | ||
| result := schema.Exec(ctx, maliciousQuery, "", nil) | ||
| // We expect either: | ||
| // 1. An error indicating the query is too complex (with fix) | ||
| // 2. Success (but it should be fast) | ||
| if result.Errors != nil { | ||
| t.Logf("Query returned errors (expected with fix): %v", result.Errors) | ||
| } | ||
| close(done) | ||
| }() | ||
|
|
||
| select { | ||
| case <-done: | ||
| t.Log("Query completed") | ||
| case <-ctx.Done(): | ||
| t.Fatal("Query timed out - DDoS vulnerability confirmed") | ||
| } | ||
| } | ||
|
|
||
| // TestDDoSVulnerability_ExtremeCase tests an even more extreme case | ||
| // This test demonstrates the vulnerability and is skipped by default. | ||
| func TestDDoSVulnerability_ExtremeCase(t *testing.T) { | ||
| t.Skip("Skipping extreme vulnerability demonstration test - it would timeout without the fix") | ||
| // Create a query with an extreme number of fields (like the user's example) | ||
| // The user's query had roughly 100,000+ fields | ||
| numFields := 100000 | ||
| fields := make([]string, numFields) | ||
| for i := 0; i < numFields; i++ { | ||
| fields[i] = "a" | ||
| } | ||
|
|
||
| maliciousQuery := "query { " + strings.Join(fields, " ") + " }" | ||
|
|
||
| schema := graphql.MustParseSchema(simpleSchema, &simpleResolver{}) | ||
|
|
||
| // Set a strict timeout | ||
| ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) | ||
| defer cancel() | ||
|
|
||
| done := make(chan struct{}) | ||
| var testErr error | ||
| go func() { | ||
| start := time.Now() | ||
| result := schema.Exec(ctx, maliciousQuery, "", nil) | ||
| duration := time.Since(start) | ||
|
|
||
| t.Logf("Query took %v to complete", duration) | ||
|
|
||
| // With a fix, this should be rejected quickly (< 100ms) | ||
| // Without a fix, this will timeout | ||
| if duration > 1*time.Second { | ||
| testErr = nil // Will be caught by timeout | ||
| } | ||
|
|
||
| if result.Errors != nil { | ||
| t.Logf("Query returned errors: %v", result.Errors) | ||
| } | ||
| close(done) | ||
| }() | ||
|
|
||
| select { | ||
| case <-done: | ||
| if testErr != nil { | ||
| t.Fatal(testErr) | ||
| } | ||
| t.Log("Query completed (should be fast with fix)") | ||
| case <-ctx.Done(): | ||
| t.Fatal("Query timed out - DDoS vulnerability confirmed. This query with 100k fields should be rejected immediately.") | ||
| } | ||
| } | ||
|
|
||
| // TestDDoSVulnerability_ValidationOnly tests that the validation phase itself is vulnerable | ||
| // This test demonstrates the vulnerability and is skipped by default. | ||
| func TestDDoSVulnerability_ValidationOnly(t *testing.T) { | ||
| t.Skip("Skipping validation-only vulnerability demonstration test - it would timeout without the fix") | ||
| // Test just the validation without execution | ||
| numFields := 10000 | ||
| fields := make([]string, numFields) | ||
| for i := 0; i < numFields; i++ { | ||
| fields[i] = "a" | ||
| } | ||
|
|
||
| maliciousQuery := "query { " + strings.Join(fields, " ") + " }" | ||
|
|
||
| schema := graphql.MustParseSchema(simpleSchema, &simpleResolver{}) | ||
|
|
||
| start := time.Now() | ||
| errors := schema.Validate(maliciousQuery) | ||
| duration := time.Since(start) | ||
|
|
||
| t.Logf("Validation took %v", duration) | ||
| t.Logf("Validation errors: %v", errors) | ||
|
|
||
| // Without a fix, validation can take seconds for 10k fields | ||
| // With a fix, it should be rejected immediately (< 100ms) | ||
| if duration > 500*time.Millisecond { | ||
| t.Errorf("Validation took too long (%v). This indicates a DDoS vulnerability in the validation phase.", duration) | ||
| } | ||
| } | ||
|
|
||
| // TestDDoSVulnerability_WithFix tests that the fix prevents the attack | ||
| func TestDDoSVulnerability_WithFix(t *testing.T) { | ||
| // Create a query with many fields | ||
| numFields := 10000 | ||
| fields := make([]string, numFields) | ||
| for i := 0; i < numFields; i++ { | ||
| fields[i] = "a" | ||
| } | ||
|
|
||
| maliciousQuery := "query { " + strings.Join(fields, " ") + " }" | ||
|
|
||
| // Create schema with MaxSelectionSetSize limit | ||
| schema := graphql.MustParseSchema(simpleSchema, &simpleResolver{}, graphql.MaxSelectionSetSize(100)) | ||
|
|
||
| start := time.Now() | ||
| errors := schema.Validate(maliciousQuery) | ||
| duration := time.Since(start) | ||
|
|
||
| t.Logf("Validation with fix took %v", duration) | ||
| t.Logf("Validation errors: %v", errors) | ||
|
|
||
| // With the fix, the query should be rejected immediately | ||
| if len(errors) == 0 { | ||
| t.Fatal("Expected validation errors, but got none") | ||
| } | ||
|
|
||
| // Check that the error message mentions the max selection set size | ||
| found := false | ||
| for _, err := range errors { | ||
| if strings.Contains(err.Message, "exceeds the maximum allowed size") { | ||
| found = true | ||
| break | ||
| } | ||
| } | ||
| if !found { | ||
| t.Errorf("Expected error about exceeding max selection set size, but got: %v", errors) | ||
| } | ||
|
|
||
| // Validation should be fast (< 100ms) | ||
| if duration > 100*time.Millisecond { | ||
| t.Errorf("Validation took too long (%v) even with the fix", duration) | ||
| } | ||
| } | ||
|
|
||
| // TestDDoSVulnerability_FixWithReasonableQuery tests that the fix doesn't break reasonable queries | ||
| func TestDDoSVulnerability_FixWithReasonableQuery(t *testing.T) { | ||
| // Create a reasonable query with just a few fields | ||
| reasonableQuery := "query { a a a a a }" | ||
|
|
||
| // Create schema with MaxSelectionSetSize limit | ||
| schema := graphql.MustParseSchema(simpleSchema, &simpleResolver{}, graphql.MaxSelectionSetSize(100)) | ||
|
|
||
| errors := schema.Validate(reasonableQuery) | ||
|
|
||
| t.Logf("Validation errors for reasonable query: %v", errors) | ||
|
|
||
| // This should not be blocked | ||
| if len(errors) > 0 { | ||
| // Check if there's an error about max selection set size | ||
| for _, err := range errors { | ||
| if strings.Contains(err.Message, "exceeds the maximum allowed size") { | ||
| t.Errorf("Reasonable query was incorrectly blocked by MaxSelectionSetSize") | ||
| } | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,14 @@ | ||
| module github.com/graph-gophers/graphql-go | ||
|
|
||
| go 1.16 | ||
| go 1.18 | ||
|
|
||
| require ( | ||
| github.com/opentracing/opentracing-go v1.2.0 | ||
| go.opentelemetry.io/otel v1.6.3 | ||
| go.opentelemetry.io/otel/trace v1.6.3 | ||
| ) | ||
|
|
||
| require ( | ||
| github.com/go-logr/logr v1.2.3 // indirect | ||
| github.com/go-logr/stdr v1.2.2 // indirect | ||
| ) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.