Skip to content

Commit

Permalink
feat(security): graphql batch attack middleware
Browse files Browse the repository at this point in the history
  • Loading branch information
Karol Nowak committed Dec 14, 2023
1 parent 2a52972 commit 6b4ec11
Show file tree
Hide file tree
Showing 2 changed files with 106 additions and 0 deletions.
102 changes: 102 additions & 0 deletions batchMiddleware.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package graphql

import (
"context"

gql "github.com/99designs/gqlgen/graphql"
"github.com/vektah/gqlparser/v2/ast"
)

const (
sameOperationsDefaultThreshold = 2
allOperationsDefaultThreshold = 10
)

func BatchMiddleware(
cfg *struct {
SameOperationsThreshold int `inject:"config:graphql.batchMiddleware.sameOperationsThreshold,optional"`
AllOperationsThreshold int `inject:"config:graphql.batchMiddleware.sameOperationsThreshold,optional"`
},
) func(ctx context.Context, next gql.OperationHandler) gql.ResponseHandler {
return func(ctx context.Context, next gql.OperationHandler) gql.ResponseHandler {
var sameOperationsThreshold int
var allOperationsThreshold int

Check failure on line 23 in batchMiddleware.go

View workflow job for this annotation

GitHub Actions / lint

declarations should never be cuddled (wsl)

if cfg == nil {
sameOperationsThreshold = sameOperationsDefaultThreshold
allOperationsThreshold = allOperationsDefaultThreshold
} else {
sameOperationsThreshold = cfg.SameOperationsThreshold
allOperationsThreshold = cfg.AllOperationsThreshold
}

req := gql.GetOperationContext(ctx)

occurrences := countTopLevelGraphQLOperations(req.Operation.SelectionSet)

countGraphqlFunctionsCalled(occurrences, req.Operation.SelectionSet)

if isAboveThreshold(sameOperationsThreshold, allOperationsThreshold, occurrences) {
return func(ctx context.Context) *gql.Response {
return gql.ErrorResponse(ctx, "request not allowed")
}
}

return next(ctx)
}
}

func countTopLevelGraphQLOperations(definition []ast.Selection) map[string]int {
mapOfOccurrences := make(map[string]int)

for _, set := range definition {
field, ok := set.(*ast.Field)
if !ok {
continue
}

if _, exists := mapOfOccurrences[field.Name]; !exists {
mapOfOccurrences[field.Name] = 0
}

mapOfOccurrences[field.Name]++
}

return mapOfOccurrences
}

func countGraphqlFunctionsCalled(mapOfOccurrences map[string]int, definition []ast.Selection) {
for _, set := range definition {
field, ok := set.(*ast.Field)
if !ok {
continue
}

// counting arguments is the only way to tell if this field is a function call
if len(field.Arguments) != 0 {
if _, exists := mapOfOccurrences[field.Name]; !exists {
mapOfOccurrences[field.Name] = 0
}

mapOfOccurrences[field.Name]++
}

if len(field.SelectionSet) > 0 {
countGraphqlFunctionsCalled(mapOfOccurrences, field.SelectionSet)
}
}
}

func isAboveThreshold(threshold, operationsThreshold int, operations map[string]int) bool {
if len(operations) > operationsThreshold {
return true
}

for _, operationsNumber := range operations {
if operationsNumber > threshold {
return true
}
}

return false
}
4 changes: 4 additions & 0 deletions module.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,10 @@ graphql: {
multipartForm: {
uploadMaxSize: (int | *1.5M) & > 0
}
batchMiddleware: {
sameOperationsThreshold: number | *3
allOperationsThreshold: number | *10
}
}
`
}

0 comments on commit 6b4ec11

Please sign in to comment.