Compare commits

...
Author SHA1 Message Date
Ice3man a1f5dc92cf misc changes 2024-10-13 02:50:42 +05:30
Ice3man 4faf08893a feat: fixed bugs + stricter validations + tests 2024-10-10 23:41:47 +05:30
Ice3man 27da4affee feat: added initial graphql fuzzing support 2024-10-10 02:39:17 +05:30
7 changed files with 410 additions and 1 deletions

No files matched your search

+1
View File
@@ -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
+2
View File
@@ -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=
+7
View File
@@ -3,6 +3,7 @@ package component
import (
"bytes"
"context"
"fmt"
"io"
"strconv"
"strings"
@@ -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)
+3
View File
@@ -21,6 +21,7 @@ func init() {
RegisterDataFormat(NewJSON())
RegisterDataFormat(NewXML())
RegisterDataFormat(NewRaw())
RegisterDataFormat(NewGraphql())
RegisterDataFormat(NewForm())
RegisterDataFormat(NewMultiPartForm())
}
@@ -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
+278
View File
@@ -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"
}
+111
View File
@@ -0,0 +1,111 @@
package dataformat
import (
"strings"
"testing"
"github.com/stretchr/testify/require"
)
const (
graphqlQueryWithInlineArgs = `{
"query": "\n query {\n jobs(jobType: \"front-end\") {\n id\n name\n type\n description\n }\n }\n "
}`
graphqlQueryWithVariables = `{
"query": "mutation ImportPaste ($host: String!, $port: Int!, $path: String!, $scheme: String!) {\n importPaste(host: $host, port: $port, path: $path, scheme: $scheme) {\n result\n }\n }",
"variables": {
"host": "example.com",
"port": 80,
"path": "/robots.txt",
"scheme": "http"
}
}`
)
func Test_GraphQL_IsGraphQL(t *testing.T) {
graphql := NewGraphql()
require.True(
t,
graphql.IsType(graphqlQueryWithInlineArgs),
"expected query to be detected as graphql",
)
require.False(
t,
graphql.IsType("not a graphql query"),
"expected query to not be detected as graphql",
)
require.False(
t,
graphql.IsType(`{"query": "not a graphql query"}`),
"expected query to not be detected as graphql",
)
}
func Test_GraphQL_DecodeEncode_InlineArgs(t *testing.T) {
decodeQueryGetKV := func(query string) (KV, map[string]interface{}, *Graphql) {
graphql := NewGraphql()
decoded, err := graphql.Decode(query)
require.Nil(t, err, "could not decode graphql query")
keyValues := make(map[string]interface{})
decoded.Iterate(func(key string, value interface{}) bool {
if strings.HasPrefix(key, "#_") {
return true
}
keyValues[key] = value
return true
})
return decoded, keyValues, graphql
}
// Test decoding and encoding
t.Run("inline args with variables", func(t *testing.T) {
decoded, keyValues, graphql := decodeQueryGetKV(graphqlQueryWithVariables)
require.Equal(t, map[string]interface{}{
"host": "example.com",
"port": float64(80),
"path": "/robots.txt",
"scheme": "http",
}, keyValues)
decoded.Set("path", "/robots.txt; cat /etc/passwd")
// Test encoding
encoded, err := graphql.Encode(decoded)
require.Nil(t, err, "could not encode graphql query")
_, newKeyValues, _ := decodeQueryGetKV(encoded)
require.Equal(t, "/robots.txt; cat /etc/passwd", newKeyValues["path"])
// Try to write non-string paths as well
t.Run("non-string paths", func(t *testing.T) {
decoded.Set("port", "80; cat /etc/passwd")
encoded, err = graphql.Encode(decoded)
require.Nil(t, err, "could not encode graphql query")
_, newKeyValues, _ = decodeQueryGetKV(encoded)
require.Equal(t, "80; cat /etc/passwd", newKeyValues["port"])
})
})
t.Run("inline args", func(t *testing.T) {
decoded, keyValues, graphql := decodeQueryGetKV(graphqlQueryWithInlineArgs)
require.Equal(t, map[string]interface{}{
"jobType": "front-end",
}, keyValues)
decoded.Set("jobType", "canary")
// Test encoding
encoded, err := graphql.Encode(decoded)
require.Nil(t, err, "could not encode graphql query")
_, newKeyValues, _ := decodeQueryGetKV(encoded)
require.Equal(t, map[string]interface{}{
"jobType": "canary",
}, newKeyValues)
})
}
+8 -1
View File
@@ -26,7 +26,14 @@ func NewJSON() *JSON {
// IsType returns true if the data is JSON encoded
func (j *JSON) IsType(data string) bool {
return strings.HasPrefix(data, "{") && strings.HasSuffix(data, "}")
isJSON := strings.HasPrefix(data, "{") && strings.HasSuffix(data, "}")
if isJSON {
// Check if its GraphQL
if Get(GraphqlDataFormat).IsType(data) {
return false
}
}
return isJSON
}
// Encode encodes the data into JSON format