diff --git a/v2/pkg/protocols/common/expressions/variables.go b/v2/pkg/protocols/common/expressions/variables.go new file mode 100644 index 00000000..01a1e2b0 --- /dev/null +++ b/v2/pkg/protocols/common/expressions/variables.go @@ -0,0 +1,32 @@ +package expressions + +import ( + "errors" + "regexp" + "strings" +) + +var unresolvedVariablesRegex = regexp.MustCompile(`\{\{(.+)\}\}`) + +// ContainsUnresolvedVariables returns an error with variable names if the passed +// input contains unresolved {{}} variables. +func ContainsUnresolvedVariables(data string) error { + matches := unresolvedVariablesRegex.FindAllStringSubmatch(data, -1) + if len(matches) == 0 { + return nil + } + errorString := &strings.Builder{} + errorString.WriteString("unresolved variables found: ") + + for i, match := range matches { + if len(match) < 2 { + continue + } + errorString.WriteString(match[1]) + if i != len(matches)-1 { + errorString.WriteString(",") + } + } + errorMessage := errorString.String() + return errors.New(errorMessage) +} diff --git a/v2/pkg/protocols/common/expressions/variables_test.go b/v2/pkg/protocols/common/expressions/variables_test.go new file mode 100644 index 00000000..55c35fff --- /dev/null +++ b/v2/pkg/protocols/common/expressions/variables_test.go @@ -0,0 +1,23 @@ +package expressions + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestUnresolvedVariablesCheck(t *testing.T) { + tests := []struct { + data string + err error + }{ + {"{{test}}", errors.New("unresolved variables found: test")}, + {"{{test}}/{{another}}", errors.New("unresolved variables found: test,another")}, + {"test", nil}, + } + for _, test := range tests { + err := ContainsUnresolvedVariables(test.data) + require.Equal(t, test.err, err, "could not get unresolved variables") + } +} diff --git a/v2/pkg/protocols/http/http.go b/v2/pkg/protocols/http/http.go index 5558e703..d79b99a2 100644 --- a/v2/pkg/protocols/http/http.go +++ b/v2/pkg/protocols/http/http.go @@ -167,6 +167,9 @@ type Request struct { // description: | // StopAtFirstMatch stops the execution of the requests and template as soon as a match is found. StopAtFirstMatch bool `yaml:"stop-at-first-match,omitempty" jsonschema:"title=stop at first match,description=Stop the execution after a match is found"` + // description: | + // SkipVariablesCheck skips the check for unresolved variables in request + SkipVariablesCheck bool `yaml:"skip-variables-check,omitempty" jsonschema:"title=skip variable checks,description=Skips the check for unresolved variables in request"` } // GetID returns the unique ID of the request if any. diff --git a/v2/pkg/protocols/http/request.go b/v2/pkg/protocols/http/request.go index d8a2c98d..0ee185e2 100644 --- a/v2/pkg/protocols/http/request.go +++ b/v2/pkg/protocols/http/request.go @@ -19,6 +19,7 @@ import ( "github.com/projectdiscovery/gologger" "github.com/projectdiscovery/nuclei/v2/pkg/output" "github.com/projectdiscovery/nuclei/v2/pkg/protocols" + "github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/expressions" "github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/generators" "github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/interactsh" "github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/tostring" @@ -272,6 +273,27 @@ func (r *Request) executeRequest(reqURL string, request *generatedRequest, previ err error ) + // For race conditions we can't dump the request body at this point as it's already waiting the open-gate event, already handled with a similar code within the race function + if !request.original.Race { + var dumpError error + dumpedRequest, dumpError = dump(request, reqURL) + if dumpError != nil { + return dumpError + } + dumpedRequestString := string(dumpedRequest) + + // Check if are there any unresolved variables. If yes, skip unless overriden by user. + if varErr := expressions.ContainsUnresolvedVariables(dumpedRequestString); varErr != nil && !r.SkipVariablesCheck { + gologger.Warning().Msgf("Could not make http request for %s: %v\n", reqURL, varErr) + return nil + } + + if r.options.Options.Debug || r.options.Options.DebugRequests { + gologger.Info().Msgf("[%s] Dumped HTTP request for %s\n\n", r.options.TemplateID, reqURL) + gologger.Print().Msgf("%s") + } + } + var formedURL string var hostname string timeStart := time.Now() @@ -310,20 +332,6 @@ func (r *Request) executeRequest(reqURL string, request *generatedRequest, previ resp, err = r.httpClient.Do(request.request) } } - - // For race conditions we can't dump the request body at this point as it's already waiting the open-gate event, already handled with a similar code within the race function - if !request.original.Race { - var dumpError error - dumpedRequest, dumpError = dump(request, reqURL) - if dumpError != nil { - return dumpError - } - - if r.options.Options.Debug || r.options.Options.DebugRequests { - gologger.Info().Msgf("[%s] Dumped HTTP request for %s\n\n", r.options.TemplateID, reqURL) - gologger.Print().Msgf("%s", string(dumpedRequest)) - } - } if err != nil { // rawhttp doesn't support draining response bodies. if resp != nil && resp.Body != nil && request.rawRequest == nil { diff --git a/v2/pkg/protocols/network/request.go b/v2/pkg/protocols/network/request.go index 61abdf3f..0fa6e6cc 100644 --- a/v2/pkg/protocols/network/request.go +++ b/v2/pkg/protocols/network/request.go @@ -139,6 +139,10 @@ func (r *Request) executeRequestWithPayloads(actualAddress, address, input strin } reqBuilder.Write(finalData) + if varErr := expressions.ContainsUnresolvedVariables(string(finalData)); varErr != nil { + gologger.Warning().Msgf("Could not make network request for %s: %v\n", actualAddress, varErr) + return nil + } if _, err := conn.Write(finalData); err != nil { r.options.Output.Request(r.options.TemplateID, address, "network", err) r.options.Progress.IncrementFailedRequestsBy(1)