From b6aca303497befd3a26c0bed38127507d2628690 Mon Sep 17 00:00:00 2001 From: Emmanuel Gautier Date: Thu, 11 Apr 2024 23:16:35 +0200 Subject: [PATCH] feat: scan introspection endpoint with get method --- scan/discover/graphql.go | 48 +++++++++++++++++++++++++---- scan/discover/graphql_test.go | 58 +++++++++++++++++++++++++++++++---- scan/graphql.go | 2 +- 3 files changed, 95 insertions(+), 13 deletions(-) diff --git a/scan/discover/graphql.go b/scan/discover/graphql.go index f2b5aa3..e362dd1 100644 --- a/scan/discover/graphql.go +++ b/scan/discover/graphql.go @@ -30,13 +30,24 @@ var potentialGraphQLEndpoints = []string{ "/v1/graphiql", "/v1/explorer", } + +const graphqlQuery = `{ + "query": "query{__schema + {queryType{name}}}" +}` + var graphqlSeclistUrl = "https://raw.githubusercontent.com/danielmiessler/SecLists/master/Discovery/Web-Content/graphql.txt" -func newGraphqlIntrospectionRequest(endpoint *url.URL) (*http.Request, error) { - return http.NewRequest(http.MethodPost, endpoint.String(), bytes.NewReader([]byte(`{ - "query": "query{__schema - {queryType{name}}}" - }`))) +func newPostGraphqlIntrospectionRequest(endpoint *url.URL) (*http.Request, error) { + return http.NewRequest(http.MethodPost, endpoint.String(), bytes.NewReader([]byte(graphqlQuery))) +} + +func newGetGraphqlIntrospectionRequest(endpoint *url.URL) (*http.Request, error) { + values := url.Values{} + values.Add("query", graphqlQuery) + endpoint.RawQuery = values.Encode() + + return http.NewRequest(http.MethodGet, endpoint.String(), nil) } func GraphqlIntrospectionScanHandler(operation *request.Operation, securityScheme auth.SecurityScheme) (*report.ScanReport, error) { @@ -47,7 +58,32 @@ func GraphqlIntrospectionScanHandler(operation *request.Operation, securitySchem base := ExtractBaseURL(operation.Request.URL) for _, path := range potentialGraphQLEndpoints { - newRequest, err := newGraphqlIntrospectionRequest(base.ResolveReference(&url.URL{Path: path})) + newRequest, err := newPostGraphqlIntrospectionRequest(base.ResolveReference(&url.URL{Path: path})) + if err != nil { + return r, err + } + + newOperation := request.NewOperationFromRequest(newRequest, []auth.SecurityScheme{securityScheme}) + attempt, err := scan.ScanURL(newOperation, &securityScheme) + r.AddScanAttempt(attempt).End() + if err != nil { + return r, err + } + + if attempt.Response.StatusCode < 300 { + r.AddVulnerabilityReport(&report.VulnerabilityReport{ + SeverityLevel: GraphqlIntrospectionEnabledSeverityLevel, + Name: GraphqlIntrospectionEnabledVulnerabilityName, + Description: GraphqlIntrospectionEnabledVulnerabilityDescription, + Operation: operation, + }) + + return r, nil + } + } + + for _, path := range potentialGraphQLEndpoints { + newRequest, err := newGetGraphqlIntrospectionRequest(base.ResolveReference(&url.URL{Path: path})) if err != nil { return r, err } diff --git a/scan/discover/graphql_test.go b/scan/discover/graphql_test.go index 40f5857..57c56c1 100644 --- a/scan/discover/graphql_test.go +++ b/scan/discover/graphql_test.go @@ -20,7 +20,26 @@ func TestGraphqlIntrospectionScanHandler(t *testing.T) { securityScheme := auth.NewNoAuthSecurityScheme() operation := request.NewOperation("http://localhost:8080", http.MethodPost, nil, nil, nil) - httpmock.RegisterResponder(operation.Method, operation.Request.URL.String(), httpmock.NewBytesResponder(204, nil).HeaderAdd(http.Header{"Server": []string{"Apache/2.4.29 (Ubuntu)"}})) + httpmock.RegisterResponder(operation.Method, operation.Request.URL.String(), httpmock.NewBytesResponder(204, nil)) + httpmock.RegisterNoResponder(func(req *http.Request) (*http.Response, error) { + return httpmock.NewStringResponse(404, "Not Found"), nil + }) + + report, err := discover.GraphqlIntrospectionScanHandler(operation, securityScheme) + + require.NoError(t, err) + assert.Greater(t, httpmock.GetTotalCallCount(), 1) + assert.False(t, report.HasVulnerabilityReport()) +} + +func TestGetGraphqlIntrospectionScanHandler(t *testing.T) { + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + securityScheme := auth.NewNoAuthSecurityScheme() + operation := request.NewOperation("http://localhost:8080", http.MethodGet, nil, nil, nil) + + httpmock.RegisterResponder(operation.Method, operation.Request.URL.String(), httpmock.NewBytesResponder(204, nil)) httpmock.RegisterNoResponder(func(req *http.Request) (*http.Response, error) { return httpmock.NewStringResponse(404, "Not Found"), nil }) @@ -38,7 +57,34 @@ func TestGraphqlIntrospectionScanHandlerWithKnownGraphQLIntrospectionEndpoint(t securityScheme := auth.NewNoAuthSecurityScheme() operation := request.NewOperation("http://localhost:8080/graphql", http.MethodPost, nil, nil, nil) - httpmock.RegisterResponder(operation.Method, operation.Request.URL.String(), httpmock.NewBytesResponder(204, nil).HeaderAdd(http.Header{"Server": []string{"Apache/2.4.29 (Ubuntu)"}})) + httpmock.RegisterResponder(operation.Method, operation.Request.URL.String(), httpmock.NewBytesResponder(204, nil)) + httpmock.RegisterNoResponder(func(req *http.Request) (*http.Response, error) { + return httpmock.NewStringResponse(404, "Not Found"), nil + }) + + expectedReport := report.VulnerabilityReport{ + SeverityLevel: discover.GraphqlIntrospectionEnabledSeverityLevel, + Name: discover.GraphqlIntrospectionEnabledVulnerabilityName, + Description: discover.GraphqlIntrospectionEnabledVulnerabilityDescription, + Operation: operation, + } + + report, err := discover.GraphqlIntrospectionScanHandler(operation, securityScheme) + + require.NoError(t, err) + assert.Greater(t, httpmock.GetTotalCallCount(), 0) + assert.True(t, report.HasVulnerabilityReport()) + assert.Equal(t, report.GetVulnerabilityReports()[0].Name, expectedReport.Name) + assert.Equal(t, report.GetVulnerabilityReports()[0].Operation.Request.URL.String(), expectedReport.Operation.Request.URL.String()) +} + +func TestGetGraphqlIntrospectionScanHandlerWithKnownGraphQLIntrospectionEndpoint(t *testing.T) { + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + securityScheme := auth.NewNoAuthSecurityScheme() + operation := request.NewOperation("http://localhost:8080/graphql", http.MethodGet, nil, nil, nil) + httpmock.RegisterResponder(operation.Method, operation.Request.URL.String(), httpmock.NewBytesResponder(204, nil)) httpmock.RegisterNoResponder(func(req *http.Request) (*http.Response, error) { return httpmock.NewStringResponse(404, "Not Found"), nil }) @@ -64,9 +110,9 @@ func TestDiscoverableScannerWithNoDiscoverableGraphqlPath(t *testing.T) { defer httpmock.DeactivateAndReset() securityScheme := auth.NewNoAuthSecurityScheme() - operation := request.NewOperation("http://localhost:8080/", "GET", nil, nil, nil) + operation := request.NewOperation("http://localhost:8080/", http.MethodGet, nil, nil, nil) - httpmock.RegisterResponder(operation.Method, operation.Request.URL.String(), httpmock.NewBytesResponder(204, nil).HeaderAdd(http.Header{"Server": []string{"Apache/2.4.29 (Ubuntu)"}})) + httpmock.RegisterResponder(operation.Method, operation.Request.URL.String(), httpmock.NewBytesResponder(204, nil)) httpmock.RegisterNoResponder(func(req *http.Request) (*http.Response, error) { return httpmock.NewStringResponse(404, "Not Found"), nil }) @@ -83,8 +129,8 @@ func TestDiscoverableScannerWithOneDiscoverableGraphQLPath(t *testing.T) { defer httpmock.DeactivateAndReset() securityScheme := auth.NewNoAuthSecurityScheme() - operation := request.NewOperation("http://localhost:8080/graphql", "GET", nil, nil, nil) - httpmock.RegisterResponder(operation.Method, operation.Request.URL.String(), httpmock.NewBytesResponder(204, nil).HeaderAdd(http.Header{"Server": []string{"Apache/2.4.29 (Ubuntu)"}})) + operation := request.NewOperation("http://localhost:8080/graphql", http.MethodGet, nil, nil, nil) + httpmock.RegisterResponder(operation.Method, operation.Request.URL.String(), httpmock.NewBytesResponder(204, nil)) httpmock.RegisterNoResponder(func(req *http.Request) (*http.Response, error) { return httpmock.NewStringResponse(404, "Not Found"), nil }) diff --git a/scan/graphql.go b/scan/graphql.go index 2155456..641243a 100644 --- a/scan/graphql.go +++ b/scan/graphql.go @@ -16,7 +16,7 @@ func NewGraphQLScan(url string, header http.Header, cookies []http.Cookie, repor securitySchemes = append(securitySchemes, auth.NewNoAuthSecurityScheme()) } - operations := request.Operations{request.NewOperation(url, "POST", header, cookies, securitySchemes)} + operations := request.Operations{request.NewOperation(url, http.MethodPost, header, cookies, securitySchemes)} return NewScan(operations, reporter) }