mirror of
https://github.com/projectdiscovery/nuclei.git
synced 2026-11-08 02:08:33 -10:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a1f5dc92cf | ||
|
|
4faf08893a | ||
|
|
27da4affee |
No files matched your search
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user