Path: blob/dev/pkg/protocols/http/request_test.go
2843 views
package http12import (3"context"4"fmt"5"net/http"6"net/http/httptest"7"sync/atomic"8"testing"9"time"1011"github.com/stretchr/testify/require"12"github.com/tarunKoyalwar/goleak"1314"github.com/projectdiscovery/nuclei/v3/pkg/model"15"github.com/projectdiscovery/nuclei/v3/pkg/model/types/severity"16"github.com/projectdiscovery/nuclei/v3/pkg/operators"17"github.com/projectdiscovery/nuclei/v3/pkg/operators/extractors"18"github.com/projectdiscovery/nuclei/v3/pkg/operators/matchers"19"github.com/projectdiscovery/nuclei/v3/pkg/output"20"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs"21"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/interactsh"22"github.com/projectdiscovery/nuclei/v3/pkg/testutils"23)2425func TestHTTPExtractMultipleReuse(t *testing.T) {26options := testutils.DefaultOptions2728testutils.Init(options)29templateID := "testing-http"30request := &Request{31ID: templateID,32Raw: []string{33`GET /robots.txt HTTP/1.134Host: {{Hostname}}35User-Agent: Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:80.0) Gecko/20100101 Firefox/80.036Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.837Accept-Language: en-US,en;q=0.538`,3940`GET {{endpoint}} HTTP/1.141Host: {{Hostname}}42User-Agent: Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:80.0) Gecko/20100101 Firefox/80.043Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.844Accept-Language: en-US,en;q=0.545`,46},47Operators: operators.Operators{48Matchers: []*matchers.Matcher{{49Part: "body",50Type: matchers.MatcherTypeHolder{MatcherType: matchers.WordsMatcher},51Words: []string{"match /a", "match /b", "match /c"},52}},53Extractors: []*extractors.Extractor{{54Part: "body",55Name: "endpoint",56Type: extractors.ExtractorTypeHolder{ExtractorType: extractors.RegexExtractor},57Regex: []string{"(?m)/([a-zA-Z0-9-_/\\\\]+)"},58Internal: true,59}},60},61IterateAll: true,62}63ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {64switch r.URL.Path {65case "/robots.txt":66_, _ = fmt.Fprintf(w, `User-agent: Googlebot67Disallow: /a68Disallow: /b69Disallow: /c`)70default:71_, _ = fmt.Fprintf(w, `match %v`, r.URL.Path)72}73}))74defer ts.Close()7576executerOpts := testutils.NewMockExecuterOptions(options, &testutils.TemplateInfo{77ID: templateID,78Info: model.Info{SeverityHolder: severity.Holder{Severity: severity.Low}, Name: "test"},79})8081err := request.Compile(executerOpts)82require.Nil(t, err, "could not compile network request")8384var finalEvent *output.InternalWrappedEvent85var matchCount int86t.Run("test", func(t *testing.T) {87metadata := make(output.InternalEvent)88previous := make(output.InternalEvent)89ctxArgs := contextargs.NewWithInput(context.Background(), ts.URL)90err := request.ExecuteWithResults(ctxArgs, metadata, previous, func(event *output.InternalWrappedEvent) {91if event.OperatorsResult != nil && event.OperatorsResult.Matched {92matchCount++93}94finalEvent = event95})96require.Nil(t, err, "could not execute network request")97})98require.NotNil(t, finalEvent, "could not get event output from request")99require.Equal(t, 3, matchCount, "could not get correct match count")100}101102func TestDisableTE(t *testing.T) {103options := testutils.DefaultOptions104105testutils.Init(options)106templateID := "http-disable-transfer-encoding"107108// in raw request format109request := &Request{110ID: templateID,111Raw: []string{112`POST / HTTP/1.1113Host: {{Hostname}}114User-Agent: Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:80.0) Gecko/20100101 Firefox/80.0115Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8116Accept-Language: en-US,en;q=0.5117118login=1&username=admin&password=admin119`,120},121Operators: operators.Operators{122Matchers: []*matchers.Matcher{{123Type: matchers.MatcherTypeHolder{MatcherType: matchers.StatusMatcher},124Status: []int{200},125}},126},127}128129// in base request format130request2 := &Request{131ID: templateID,132Method: HTTPMethodTypeHolder{MethodType: HTTPPost},133Path: []string{"{{BaseURL}}"},134Body: "login=1&username=admin&password=admin",135Operators: operators.Operators{136Matchers: []*matchers.Matcher{{137Type: matchers.MatcherTypeHolder{MatcherType: matchers.StatusMatcher},138Status: []int{200},139}},140},141}142ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {143if len(r.TransferEncoding) > 0 || r.ContentLength <= 0 {144t.Error("Transfer-Encoding header should not be set")145}146}))147defer ts.Close()148149executerOpts := testutils.NewMockExecuterOptions(options, &testutils.TemplateInfo{150ID: templateID,151Info: model.Info{SeverityHolder: severity.Holder{Severity: severity.Low}, Name: "test"},152})153154err := request.Compile(executerOpts)155require.Nil(t, err, "could not compile http raw request")156157err = request2.Compile(executerOpts)158require.Nil(t, err, "could not compile http base request")159160var finalEvent *output.InternalWrappedEvent161var matchCount int162t.Run("test", func(t *testing.T) {163metadata := make(output.InternalEvent)164previous := make(output.InternalEvent)165ctxArgs := contextargs.NewWithInput(context.Background(), ts.URL)166err := request.ExecuteWithResults(ctxArgs, metadata, previous, func(event *output.InternalWrappedEvent) {167if event.OperatorsResult != nil && event.OperatorsResult.Matched {168matchCount++169}170finalEvent = event171})172require.Nil(t, err, "could not execute network request")173})174175t.Run("test2", func(t *testing.T) {176metadata := make(output.InternalEvent)177previous := make(output.InternalEvent)178ctxArgs := contextargs.NewWithInput(context.Background(), ts.URL)179err := request2.ExecuteWithResults(ctxArgs, metadata, previous, func(event *output.InternalWrappedEvent) {180if event.OperatorsResult != nil && event.OperatorsResult.Matched {181matchCount++182}183finalEvent = event184})185require.Nil(t, err, "could not execute network request")186})187188require.NotNil(t, finalEvent, "could not get event output from request")189require.Equal(t, 2, matchCount, "could not get correct match count")190}191192// consult @Ice3man543 before making any breaking changes to this test (context: vuln_hash)193func TestReqURLPattern(t *testing.T) {194options := testutils.DefaultOptions195196// assume this was a preprocessor197// {{randstr}} => 2eNU2kbrOcUDzhnUL1RGvSo1it7198testutils.Init(options)199templateID := "testing-http"200request := &Request{201ID: templateID,202Raw: []string{203`GET /{{rand_char("abc")}}/{{interactsh-url}}/123?query={{rand_int(1, 10)}}&data=2eNU2kbrOcUDzhnUL1RGvSo1it7 HTTP/1.1204Host: {{Hostname}}205User-Agent: Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:80.0) Gecko/20100101 Firefox/80.0206Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8207Accept-Language: en-US,en;q=0.5208`,209},210Operators: operators.Operators{211Matchers: []*matchers.Matcher{{212Type: matchers.MatcherTypeHolder{MatcherType: matchers.DSLMatcher},213DSL: []string{"true"},214}},215},216IterateAll: true,217}218ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {219// always return 200220w.WriteHeader(200)221_, _ = w.Write([]byte(`match`))222}))223defer ts.Close()224225executerOpts := testutils.NewMockExecuterOptions(options, &testutils.TemplateInfo{226ID: templateID,227Info: model.Info{SeverityHolder: severity.Holder{Severity: severity.Low}, Name: "test"},228})229client, _ := interactsh.New(interactsh.DefaultOptions(executerOpts.Output, nil, executerOpts.Progress))230executerOpts.Interactsh = client231defer client.Close()232executerOpts.ExportReqURLPattern = true233234// this is how generated constants are added to template235// generated constants are preprocessors that are executed while loading once236executerOpts.Constants = map[string]interface{}{237"{{randstr}}": "2eNU2kbrOcUDzhnUL1RGvSo1it7",238}239240err := request.Compile(executerOpts)241require.Nil(t, err, "could not compile network request")242243var finalEvent *output.InternalWrappedEvent244var matchCount int245t.Run("test", func(t *testing.T) {246metadata := make(output.InternalEvent)247previous := make(output.InternalEvent)248ctxArgs := contextargs.NewWithInput(context.Background(), ts.URL)249err := request.ExecuteWithResults(ctxArgs, metadata, previous, func(event *output.InternalWrappedEvent) {250if event.OperatorsResult != nil && event.OperatorsResult.Matched {251matchCount++252}253finalEvent = event254})255require.Nil(t, err, "could not execute network request")256})257require.NotNil(t, finalEvent, "could not get event output from request")258require.Equal(t, 1, matchCount, "could not get correct match count")259require.NotEmpty(t, finalEvent.Results[0].ReqURLPattern, "could not get req url pattern")260require.Equal(t, `/{{rand_char("abc")}}/{{interactsh-url}}/123?query={{rand_int(1, 10)}}&data={{randstr}}`, finalEvent.Results[0].ReqURLPattern)261}262263// fakeHostErrorsCache implements hosterrorscache.CacheInterface minimally for tests264type fakeHostErrorsCache struct{}265266func (f *fakeHostErrorsCache) SetVerbose(bool) {}267func (f *fakeHostErrorsCache) Close() {}268func (f *fakeHostErrorsCache) Remove(*contextargs.Context) {}269func (f *fakeHostErrorsCache) MarkFailed(string, *contextargs.Context, error) {}270func (f *fakeHostErrorsCache) MarkFailedOrRemove(string, *contextargs.Context, error) {271}272273// Check always returns true to simulate an already unresponsive host274func (f *fakeHostErrorsCache) Check(string, *contextargs.Context) bool { return true }275276// IsPermanentErr returns false for tests277func (f *fakeHostErrorsCache) IsPermanentErr(*contextargs.Context, error) bool { return false }278279func TestExecuteParallelHTTP_StopAtFirstMatch(t *testing.T) {280options := testutils.DefaultOptions281testutils.Init(options)282283// server that always matches284ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {285_, _ = fmt.Fprintf(w, "match")286}))287defer ts.Close()288289templateID := "parallel-stop-first"290req := &Request{291ID: templateID,292Method: HTTPMethodTypeHolder{MethodType: HTTPGet},293Path: []string{"{{BaseURL}}/p?x={{v}}"},294Threads: 2,295Payloads: map[string]interface{}{296"v": []string{"1", "2"},297},298Operators: operators.Operators{299Matchers: []*matchers.Matcher{{300Part: "body",301Type: matchers.MatcherTypeHolder{MatcherType: matchers.WordsMatcher},302Words: []string{"match"},303}},304},305StopAtFirstMatch: true,306}307308executerOpts := testutils.NewMockExecuterOptions(options, &testutils.TemplateInfo{309ID: templateID,310Info: model.Info{SeverityHolder: severity.Holder{Severity: severity.Low}, Name: "test"},311})312err := req.Compile(executerOpts)313require.NoError(t, err)314315var matches int32316metadata := make(output.InternalEvent)317previous := make(output.InternalEvent)318ctxArgs := contextargs.NewWithInput(context.Background(), ts.URL)319err = req.ExecuteWithResults(ctxArgs, metadata, previous, func(event *output.InternalWrappedEvent) {320if event.OperatorsResult != nil && event.OperatorsResult.Matched {321atomic.AddInt32(&matches, 1)322}323})324require.NoError(t, err)325require.Equal(t, int32(1), atomic.LoadInt32(&matches), "expected only first match to be processed")326}327328func TestExecuteParallelHTTP_SkipOnUnresponsiveFromCache(t *testing.T) {329options := testutils.DefaultOptions330testutils.Init(options)331332// server that would match if reached333ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {334_, _ = fmt.Fprintf(w, "match")335}))336defer ts.Close()337338templateID := "parallel-skip-unresponsive"339req := &Request{340ID: templateID,341Method: HTTPMethodTypeHolder{MethodType: HTTPGet},342Path: []string{"{{BaseURL}}/p?x={{v}}"},343Threads: 2,344Payloads: map[string]interface{}{345"v": []string{"1", "2"},346},347Operators: operators.Operators{348Matchers: []*matchers.Matcher{{349Part: "body",350Type: matchers.MatcherTypeHolder{MatcherType: matchers.WordsMatcher},351Words: []string{"match"},352}},353},354}355356executerOpts := testutils.NewMockExecuterOptions(options, &testutils.TemplateInfo{357ID: templateID,358Info: model.Info{SeverityHolder: severity.Holder{Severity: severity.Low}, Name: "test"},359})360// inject fake host errors cache that forces skip361executerOpts.HostErrorsCache = &fakeHostErrorsCache{}362363err := req.Compile(executerOpts)364require.NoError(t, err)365366var matches int32367metadata := make(output.InternalEvent)368previous := make(output.InternalEvent)369ctxArgs := contextargs.NewWithInput(context.Background(), ts.URL)370err = req.ExecuteWithResults(ctxArgs, metadata, previous, func(event *output.InternalWrappedEvent) {371if event.OperatorsResult != nil && event.OperatorsResult.Matched {372atomic.AddInt32(&matches, 1)373}374})375require.NoError(t, err)376require.Equal(t, int32(0), atomic.LoadInt32(&matches), "expected no matches when host is marked unresponsive")377}378379// TestExecuteParallelHTTP_GoroutineLeaks uses goleak to detect goroutine leaks in all HTTP parallel execution scenarios380func TestExecuteParallelHTTP_GoroutineLeaks(t *testing.T) {381defer goleak.VerifyNone(t,382goleak.IgnoreAnyContainingPkg("go.opencensus.io/stats/view"),383goleak.IgnoreAnyContainingPkg("github.com/syndtr/goleveldb"),384goleak.IgnoreAnyContainingPkg("github.com/go-rod/rod"),385goleak.IgnoreAnyContainingPkg("github.com/projectdiscovery/interactsh/pkg/server"),386goleak.IgnoreAnyContainingPkg("github.com/projectdiscovery/interactsh/pkg/client"),387goleak.IgnoreAnyContainingPkg("github.com/projectdiscovery/ratelimit"),388goleak.IgnoreAnyFunction("github.com/syndtr/goleveldb/leveldb/util.(*BufferPool).drain"),389goleak.IgnoreAnyFunction("github.com/syndtr/goleveldb/leveldb.(*DB).compactionError"),390goleak.IgnoreAnyFunction("github.com/syndtr/goleveldb/leveldb.(*DB).mpoolDrain"),391goleak.IgnoreAnyFunction("github.com/syndtr/goleveldb/leveldb.(*DB).tCompaction"),392goleak.IgnoreAnyFunction("github.com/syndtr/goleveldb/leveldb.(*DB).mCompaction"),393)394395options := testutils.DefaultOptions396testutils.Init(options)397defer testutils.Cleanup(options)398399// Test Case 1: Normal execution with StopAtFirstMatch400t.Run("StopAtFirstMatch", func(t *testing.T) {401ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {402time.Sleep(10 * time.Millisecond)403_, _ = fmt.Fprintf(w, "test response")404}))405defer ts.Close()406407req := &Request{408ID: "parallel-stop-first-match",409Method: HTTPMethodTypeHolder{MethodType: HTTPGet},410Path: []string{"{{BaseURL}}/test?param={{payload}}"},411Threads: 4,412Payloads: map[string]interface{}{413"payload": []string{"1", "2", "3", "4", "5", "6", "7", "8"},414},415Operators: operators.Operators{416Matchers: []*matchers.Matcher{{417Part: "body",418Type: matchers.MatcherTypeHolder{MatcherType: matchers.WordsMatcher},419Words: []string{"test response"},420}},421},422StopAtFirstMatch: true,423}424425executerOpts := testutils.NewMockExecuterOptions(options, &testutils.TemplateInfo{426ID: "parallel-stop-first-match",427Info: model.Info{SeverityHolder: severity.Holder{Severity: severity.Low}, Name: "test"},428})429430err := req.Compile(executerOpts)431require.NoError(t, err)432433metadata := make(output.InternalEvent)434previous := make(output.InternalEvent)435ctxArgs := contextargs.NewWithInput(context.Background(), ts.URL)436437err = req.ExecuteWithResults(ctxArgs, metadata, previous, func(event *output.InternalWrappedEvent) {})438require.NoError(t, err)439})440441// Test Case 2: Unresponsive host scenario442t.Run("UnresponsiveHost", func(t *testing.T) {443ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {444_, _ = fmt.Fprintf(w, "response")445}))446defer ts.Close()447448req := &Request{449ID: "parallel-unresponsive",450Method: HTTPMethodTypeHolder{MethodType: HTTPGet},451Path: []string{"{{BaseURL}}/test?param={{payload}}"},452Threads: 3,453Payloads: map[string]interface{}{454"payload": []string{"1", "2", "3", "4", "5"},455},456Operators: operators.Operators{457Matchers: []*matchers.Matcher{{458Part: "body",459Type: matchers.MatcherTypeHolder{MatcherType: matchers.WordsMatcher},460Words: []string{"response"},461}},462},463}464465executerOpts := testutils.NewMockExecuterOptions(options, &testutils.TemplateInfo{466ID: "parallel-unresponsive",467Info: model.Info{SeverityHolder: severity.Holder{Severity: severity.Low}, Name: "test"},468})469executerOpts.HostErrorsCache = &fakeHostErrorsCache{}470471err := req.Compile(executerOpts)472require.NoError(t, err)473474metadata := make(output.InternalEvent)475previous := make(output.InternalEvent)476ctxArgs := contextargs.NewWithInput(context.Background(), ts.URL)477478err = req.ExecuteWithResults(ctxArgs, metadata, previous, func(event *output.InternalWrappedEvent) {})479require.NoError(t, err)480})481482// Test Case 3: Context cancellation scenario483t.Run("ContextCancellation", func(t *testing.T) {484ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {485time.Sleep(200 * time.Millisecond)486_, _ = fmt.Fprintf(w, "response")487}))488defer ts.Close()489490req := &Request{491ID: "parallel-context-cancel",492Method: HTTPMethodTypeHolder{MethodType: HTTPGet},493Path: []string{"{{BaseURL}}/test?param={{payload}}"},494Threads: 3,495Payloads: map[string]interface{}{496"payload": []string{"1", "2", "3", "4", "5"},497},498Operators: operators.Operators{499Matchers: []*matchers.Matcher{{500Part: "body",501Type: matchers.MatcherTypeHolder{MatcherType: matchers.WordsMatcher},502Words: []string{"response"},503}},504},505}506507executerOpts := testutils.NewMockExecuterOptions(options, &testutils.TemplateInfo{508ID: "parallel-context-cancel",509Info: model.Info{SeverityHolder: severity.Holder{Severity: severity.Low}, Name: "test"},510})511512err := req.Compile(executerOpts)513require.NoError(t, err)514515metadata := make(output.InternalEvent)516previous := make(output.InternalEvent)517518ctx, cancel := context.WithCancel(context.Background())519ctxArgs := contextargs.NewWithInput(ctx, ts.URL)520521go func() {522time.Sleep(50 * time.Millisecond)523cancel()524}()525526err = req.ExecuteWithResults(ctxArgs, metadata, previous, func(event *output.InternalWrappedEvent) {})527require.Error(t, err)528require.Equal(t, context.Canceled, err)529})530}531532533