Skip to content
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

feat: added initial graphql fuzzing support #5716

Draft
wants to merge 3 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
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
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ require (
github.com/go-ldap/ldap/v3 v3.4.5
github.com/go-pg/pg v8.0.7+incompatible
github.com/go-sql-driver/mysql v1.7.1
github.com/graphql-go/graphql v0.8.1
github.com/h2non/filetype v1.1.3
github.com/invopop/yaml v0.3.1
github.com/kitabisa/go-ci v1.0.3
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -528,6 +528,8 @@ github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2z
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/graphql-go/graphql v0.8.1 h1:p7/Ou/WpmulocJeEx7wjQy611rtXGQaAcXGqanuMMgc=
github.com/graphql-go/graphql v0.8.1/go.mod h1:nKiHzRM0qopJEwCITUuIsxk9PlVlwIiiI8pnJEhordQ=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
Expand Down
7 changes: 7 additions & 0 deletions pkg/fuzz/component/body.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package component
import (
"bytes"
"context"
"fmt"
"io"
"strconv"
"strings"
Expand Down Expand Up @@ -62,6 +63,12 @@ func (b *Body) Parse(req *retryablehttp.Request) (bool, error) {

switch {
case strings.Contains(contentType, "application/json") && tmp.IsNIL():
// In case its a json body, check if the underlying data
// is graphql if so, parse it as graphql
if dataformat.Get(dataformat.GraphqlDataFormat).IsType(dataStr) {
fmt.Printf("dataStr: %s\n", dataStr)
return b.parseBody(dataformat.GraphqlDataFormat, req)
}
return b.parseBody(dataformat.JSONDataFormat, req)
case strings.Contains(contentType, "application/xml") && tmp.IsNIL():
return b.parseBody(dataformat.XMLDataFormat, req)
Expand Down
3 changes: 3 additions & 0 deletions pkg/fuzz/dataformat/dataformat.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ func init() {
RegisterDataFormat(NewJSON())
RegisterDataFormat(NewXML())
RegisterDataFormat(NewRaw())
RegisterDataFormat(NewGraphql())
RegisterDataFormat(NewForm())
RegisterDataFormat(NewMultiPartForm())
}
Expand All @@ -36,6 +37,8 @@ const (
FormDataFormat = "form"
// MultiPartFormDataFormat is the name of the MultiPartForm data format
MultiPartFormDataFormat = "multipart/form-data"
// GraphqlDataFormat is the name of the Graphql data format
GraphqlDataFormat = "graphql"
)

// Get returns the dataformat by name
Expand Down
278 changes: 278 additions & 0 deletions pkg/fuzz/dataformat/graphql.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,278 @@
package dataformat

import (
"encoding/json"
"fmt"
"log"
"strings"

"github.com/graphql-go/graphql/language/kinds"
"github.com/graphql-go/graphql/language/parser"
"github.com/graphql-go/graphql/language/printer"
"github.com/graphql-go/graphql/language/source"
jsoniter "github.com/json-iterator/go"
"github.com/pkg/errors"
"github.com/projectdiscovery/nuclei/v3/pkg/types"

"github.com/graphql-go/graphql/language/ast"
)

type Graphql struct{}

var (
_ DataFormat = &Graphql{}
)

// NewGraphql returns a new GraphQL encoder
func NewGraphql() *Graphql {
return &Graphql{}
}

// IsType returns true if the data is Graqhql encoded
func (m *Graphql) IsType(data string) bool {
_, isGraphql, _ := isGraphQLOperation([]byte(data), true)
return isGraphql
}

// consider it container type of our graphql representation
type graphQLRequest struct {
Query string `json:"query,omitempty"`
OperationName string `json:"operationName,omitempty"`
Variables map[string]interface{} `json:"variables,omitempty"`
}

func isGraphQLOperation(jsonData []byte, validate bool) (graphQLRequest, bool, error) {
jsonStr := string(jsonData)
if !strings.HasPrefix(jsonStr, "{") && !strings.HasSuffix(jsonStr, "}") {
return graphQLRequest{}, false, nil
}

var request graphQLRequest
if err := json.Unmarshal(jsonData, &request); err != nil {
return graphQLRequest{}, false, errors.Wrap(err, "could not unmarshal json")
}

if request.Query == "" && request.OperationName == "" && len(request.Variables) == 0 {
return graphQLRequest{}, false, nil
}

// Validate if query actually is a graphql
// query and not just some random json
if !validate {
return request, true, nil
}

doc, err := parser.Parse(parser.ParseParams{
Source: &source.Source{
Body: []byte(request.Query),
},
})
if err != nil {
return graphQLRequest{}, false, err
}
if len(doc.Definitions) == 0 {
return graphQLRequest{}, false, nil
}

return request, true, nil
}

// Encode encodes the data into MultiPartForm format
func (m *Graphql) Encode(data KV) (string, error) {
parsedRequest := data.Get("#_parsedReq")
if parsedRequest == nil {
return "", fmt.Errorf("parsed request not found")
}
parsedRequestStruct, ok := parsedRequest.(graphQLRequest)
if !ok {
return "", fmt.Errorf("parsed request is not of type graphQLRequest")
}

_, astDoc, err := m.parseGraphQLRequest(parsedRequestStruct.Query, false)
if err != nil {
return "", fmt.Errorf("error parsing graphql request: %v", err)
}

var hasVariables bool
if hasVariablesItem := data.Get("#_hasVariables"); hasVariablesItem != nil {
hasVariables, _ = hasVariablesItem.(bool)
}

data.Iterate(func(key string, value any) bool {
if strings.HasPrefix(key, "#_") {
return true
}

if hasVariables {
parsedRequestStruct.Variables[key] = value
return true
}
if err := m.modifyASTWithKeyValue(astDoc, key, value); err != nil {
log.Printf("error modifying ast with key value: %v", err)
return false
}
return true
})

modifiedQuery := printer.Print(astDoc)
parsedRequestStruct.Query = types.ToString(modifiedQuery)

marshalled, err := jsoniter.Marshal(parsedRequestStruct)
if err != nil {
return "", fmt.Errorf("error marshalling parsed request: %v", err)
}
return string(marshalled), nil
}

func (m *Graphql) modifyASTWithKeyValue(astDoc *ast.Document, key string, value any) error {
for _, def := range astDoc.Definitions {
switch v := def.(type) {
case *ast.OperationDefinition:
if v.SelectionSet == nil {
continue
}

for _, selection := range v.SelectionSet.Selections {
switch field := selection.(type) {
case *ast.Field:
for _, arg := range field.Arguments {
if arg.Name.Value == key {
arg.Value = convertGoValueToASTValue(value)
}
}
}
}
}
}
return nil
}

// Decode decodes the data from Graphql format
func (m *Graphql) Decode(data string) (KV, error) {
parsedReq, astDoc, err := m.parseGraphQLRequest(data, true)
if err != nil {
return KV{}, fmt.Errorf("error parsing graphql request: %v", err)
}

kv := KVMap(map[string]interface{}{})
kv.Set("#_parsedReq", parsedReq)

for k, v := range parsedReq.Variables {
kv.Set(k, v)
}
if len(parsedReq.Variables) > 0 {
kv.Set("#_hasVariables", true)
}
if err := m.populateGraphQLKV(astDoc, kv); err != nil {
return KV{}, fmt.Errorf("error populating graphql kv: %v", err)
}
return kv, nil
}

func (m *Graphql) populateGraphQLKV(astDoc *ast.Document, kv KV) error {
for _, def := range astDoc.Definitions {
switch def := def.(type) {
case *ast.OperationDefinition:
args, err := getSelectionSetArguments(def)
if err != nil {
return fmt.Errorf("error getting selection set arguments: %v", err)
}

for k, v := range args {
if item := kv.Get(k); item != nil {
continue
}
kv.Set(k, v)
}
}
}
return nil
}

func (m *Graphql) parseGraphQLRequest(query string, unmarshal bool) (graphQLRequest, *ast.Document, error) {
var parsedReq graphQLRequest
var err error

if unmarshal {
parsedReq, _, err = isGraphQLOperation([]byte(query), false)
if err != nil {
return graphQLRequest{}, nil, fmt.Errorf("error parsing query: %v", err)
}
} else {
parsedReq.Query = query
}

astDoc, err := parser.Parse(parser.ParseParams{
Source: &source.Source{
Body: []byte(parsedReq.Query),
},
})
if err != nil {
return graphQLRequest{}, nil, fmt.Errorf("error parsing ast: %v", err)
}
return parsedReq, astDoc, nil
}

func getSelectionSetArguments(def *ast.OperationDefinition) (map[string]interface{}, error) {
args := make(map[string]interface{})

if def.SelectionSet == nil {
return args, nil
}
for _, selection := range def.SelectionSet.Selections {
switch field := selection.(type) {
case *ast.Field:
for _, arg := range field.Arguments {
args[arg.Name.Value] = convertValueToGoType(arg.Value)
}
}
}
return args, nil
}

func convertGoValueToASTValue(value any) ast.Value {
switch v := value.(type) {
case string:
newValue := &ast.StringValue{
Kind: kinds.StringValue,
Value: v,
}
return newValue
}
return nil
}

func convertValueToGoType(value ast.Value) interface{} {
switch value := value.(type) {
case *ast.StringValue:
return value.Value
case *ast.IntValue:
return value.Value
case *ast.FloatValue:
return value.Value
case *ast.BooleanValue:
return value.Value
case *ast.EnumValue:
return value.Value
case *ast.ListValue:
var list []interface{}
for _, v := range value.Values {
list = append(list, convertValueToGoType(v))
}
return list
case *ast.ObjectValue:
obj := make(map[string]interface{})
for _, v := range value.Fields {
obj[v.Name.Value] = convertValueToGoType(v.Value)
}
return obj
}

return nil

}

// Name returns the name of the encoder
func (m *Graphql) Name() string {
return "graphql"
}
Loading
Loading