diff --git a/pkg/fuzz/analyzers/analyzers.go b/pkg/fuzz/analyzers/analyzers.go index 6266e8bb01..673039ac25 100644 --- a/pkg/fuzz/analyzers/analyzers.go +++ b/pkg/fuzz/analyzers/analyzers.go @@ -1,96 +1,111 @@ -package analyzers - -import ( - "math/rand" - "strconv" - "strings" - "time" - - "github.com/projectdiscovery/nuclei/v3/pkg/fuzz" - "github.com/projectdiscovery/retryablehttp-go" -) - -// Analyzer is an interface for all the analyzers -// that can be used for the fuzzer -type Analyzer interface { - // Name returns the name of the analyzer - Name() string - // ApplyTransformation applies the transformation to the initial payload. - ApplyInitialTransformation(data string, params map[string]interface{}) string - // Analyze is the main function for the analyzer - Analyze(options *Options) (bool, string, error) -} - -// AnalyzerTemplate is the template for the analyzer -type AnalyzerTemplate struct { - // description: | - // Name is the name of the analyzer to use - // values: - // - time_delay - Name string `json:"name" yaml:"name"` - // description: | - // Parameters is the parameters for the analyzer - // - // Parameters are different for each analyzer. For example, you can customize - // time_delay analyzer with sleep_duration, time_slope_error_range, etc. Refer - // to the docs for each analyzer to get an idea about parameters. - Parameters map[string]interface{} `json:"parameters" yaml:"parameters"` -} - -var ( - analyzers map[string]Analyzer -) - -// RegisterAnalyzer registers a new analyzer -func RegisterAnalyzer(name string, analyzer Analyzer) { - analyzers[name] = analyzer -} - -// GetAnalyzer returns the analyzer for a given name -func GetAnalyzer(name string) Analyzer { - return analyzers[name] -} - -func init() { - analyzers = make(map[string]Analyzer) -} - -// Options contains the options for the analyzer -type Options struct { - FuzzGenerated fuzz.GeneratedRequest - HttpClient *retryablehttp.Client - ResponseTimeDelay time.Duration - AnalyzerParameters map[string]interface{} -} - -var ( - random = rand.New(rand.NewSource(time.Now().UnixNano())) -) - -// ApplyPayloadTransformations applies the payload transformations to the payload -// It supports the below payloads - -// - [RANDNUM] => random number between 1000 and 9999 -// - [RANDSTR] => random string of 4 characters -func ApplyPayloadTransformations(value string) string { - randomInt := GetRandomInteger() - randomStr := randStringBytesMask(4) - - value = strings.ReplaceAll(value, "[RANDNUM]", strconv.Itoa(randomInt)) - value = strings.ReplaceAll(value, "[RANDSTR]", randomStr) - return value -} - -const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" - -func randStringBytesMask(n int) string { - b := make([]byte, n) - for i := range b { - b[i] = letterBytes[random.Intn(len(letterBytes))] - } - return string(b) -} - -// GetRandomInteger returns a random integer between 1000 and 9999 -func GetRandomInteger() int { - return random.Intn(9000) + 1000 -} +package analyzers + +import ( + "math/rand" + "strconv" + "strings" + "sync" + "time" + + "github.com/projectdiscovery/nuclei/v3/pkg/fuzz" + "github.com/projectdiscovery/retryablehttp-go" +) + +// Analyzer is an interface for all the analyzers +// that can be used for the fuzzer +type Analyzer interface { + // Name returns the name of the analyzer + Name() string + // ApplyTransformation applies the transformation to the initial payload. + ApplyInitialTransformation(data string, params map[string]interface{}) string + // Analyze is the main function for the analyzer + Analyze(options *Options) (bool, string, error) +} + +// AnalyzerTemplate is the template for the analyzer +type AnalyzerTemplate struct { + // description: | + // Name is the name of the analyzer to use + // values: + // - time_delay + // - xss_context + Name string `json:"name" yaml:"name"` + // description: | + // Parameters is the parameters for the analyzer + // + // Parameters are different for each analyzer. For example, you can customize + // time_delay analyzer with sleep_duration, time_slope_error_range, etc. Refer + // to the docs for each analyzer to get an idea about parameters. + Parameters map[string]interface{} `json:"parameters" yaml:"parameters"` +} + +var ( + analyzers map[string]Analyzer +) + +// RegisterAnalyzer registers a new analyzer +func RegisterAnalyzer(name string, analyzer Analyzer) { + analyzers[name] = analyzer +} + +// GetAnalyzer returns the analyzer for a given name +func GetAnalyzer(name string) Analyzer { + return analyzers[name] +} + +func init() { + analyzers = make(map[string]Analyzer) +} + +// Options contains the options for the analyzer +type Options struct { + FuzzGenerated fuzz.GeneratedRequest + HttpClient *retryablehttp.Client + ResponseTimeDelay time.Duration + AnalyzerParameters map[string]interface{} + + // ResponseBody is the body of the HTTP response + ResponseBody string + // ResponseHeaders is the headers of the HTTP response + ResponseHeaders map[string][]string + // ResponseStatusCode is the status code of the HTTP response + ResponseStatusCode int +} + +var ( + random = rand.New(rand.NewSource(time.Now().UnixNano())) + randomMu sync.Mutex +) + +// ApplyPayloadTransformations applies the payload transformations to the payload +// It supports the below payloads - +// - [RANDNUM] => random number between 1000 and 9999 +// - [RANDSTR] => random string of 4 characters +func ApplyPayloadTransformations(value string) string { + randomInt := GetRandomInteger() + randomStr := RandStringBytesMask(4) + + value = strings.ReplaceAll(value, "[RANDNUM]", strconv.Itoa(randomInt)) + value = strings.ReplaceAll(value, "[RANDSTR]", randomStr) + return value +} + +const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + +// RandStringBytesMask generates a random string of n characters +func RandStringBytesMask(n int) string { + randomMu.Lock() + defer randomMu.Unlock() + b := make([]byte, n) + for i := range b { + b[i] = letterBytes[random.Intn(len(letterBytes))] + } + return string(b) +} + +// GetRandomInteger returns a random integer between 1000 and 9999 +func GetRandomInteger() int { + randomMu.Lock() + defer randomMu.Unlock() + return random.Intn(9000) + 1000 +} diff --git a/pkg/fuzz/analyzers/xss/analyzer.go b/pkg/fuzz/analyzers/xss/analyzer.go new file mode 100644 index 0000000000..2b3c665d14 --- /dev/null +++ b/pkg/fuzz/analyzers/xss/analyzer.go @@ -0,0 +1,314 @@ +package xss + +import ( + "fmt" + "io" + "net/http" + "regexp" + "strings" + + "github.com/pkg/errors" + "github.com/projectdiscovery/gologger" + "github.com/projectdiscovery/nuclei/v3/pkg/fuzz/analyzers" +) + +// Analyzer is an XSS context analyzer for the fuzzer. +// It detects the reflection context of injected payloads and +// verifies XSS by replaying context-appropriate payloads. +type Analyzer struct{} + +var _ analyzers.Analyzer = &Analyzer{} + +func init() { + analyzers.RegisterAnalyzer("xss_context", &Analyzer{}) +} + +// Name returns the name of the analyzer +func (a *Analyzer) Name() string { + return "xss_context" +} + +// ApplyInitialTransformation replaces placeholder tokens in the payload. +// +// It supports: +// - [XSS_CANARY] => unique canary string with XSS-critical characters for reflection/context detection +// +// It also applies the standard [RANDNUM] and [RANDSTR] transformations. +func (a *Analyzer) ApplyInitialTransformation(data string, params map[string]interface{}) string { + if strings.Contains(data, "[XSS_CANARY]") { + canary := generateCanary() + if params != nil { + params["xss_canary"] = canary + } + // The canary includes special chars for character survival detection + canaryWithChars := canary + canaryChars + data = strings.ReplaceAll(data, "[XSS_CANARY]", canaryWithChars) + } + data = analyzers.ApplyPayloadTransformations(data) + return data +} + +// generateCanary creates a unique canary string +func generateCanary() string { + return "nuclei" + analyzers.RandStringBytesMask(8) +} + +var canaryRegexp = regexp.MustCompile(`nuclei[a-zA-Z]{8}`) + +// extractCanary extracts the analyzer canary from the final fuzzed value. +func extractCanary(value string) string { + if value == "" { + return "" + } + return canaryRegexp.FindString(value) +} + +// Analyze detects XSS vulnerabilities by: +// 1. Checking for canary reflection in the initial response +// 2. Detecting the HTML context of the reflection +// 3. Replaying context-appropriate payloads to verify exploitability +func (a *Analyzer) Analyze(options *analyzers.Options) (bool, string, error) { + // Extract canary from the final fuzzed request value to avoid shared-param desync. + canary := extractCanary(options.FuzzGenerated.Value) + if canary == "" { + return false, "", nil + } + + body := options.ResponseBody + if body == "" { + return false, "", nil + } + + // Check Content-Type - only analyze HTML responses + if !isHTMLResponse(options.ResponseHeaders) { + return false, "", nil + } + + // Check if canary is reflected at all + if !strings.Contains(body, canary) { + return false, "", nil + } + + // Detect character survival + chars := detectCharacterSurvival(body, canary) + + // Detect reflection contexts using the HTML tokenizer + reflections := DetectReflections(body, canary) + if len(reflections) == 0 { + return false, "", nil + } + + best := BestReflection(reflections) + if best == nil || best.Context == ContextNone { + return false, "", nil + } + + // Select payloads appropriate for the detected context + payloads := selectPayloads(best, chars) + if len(payloads) == 0 { + return false, "", fmt.Errorf("no suitable payloads for context %s", best.Context) + } + + // Replay with context-appropriate payloads + for _, payload := range payloads { + matched, details, err := a.replayAndVerify(options, payload, best) + if err != nil { + gologger.Verbose().Msgf("[%s] replay error: %v", a.Name(), err) + continue + } + if matched { + return true, details, nil + } + } + + return false, "", nil +} + +// replayAndVerify sends a request with the given payload and checks +// if the payload appears unencoded in the response. +func (a *Analyzer) replayAndVerify(options *analyzers.Options, payload string, reflection *ReflectionInfo) (bool, string, error) { + gr := options.FuzzGenerated + + if gr.Component == nil { + return false, "", errors.New("fuzz component is nil") + } + + origPayload := gr.OriginalPayload + + // Set the payload into the component + if err := gr.Component.SetValue(gr.Key, payload); err != nil { + return false, "", errors.Wrap(err, "could not set value in component") + } + // Always restore original value, even when rebuild fails. + defer func() { + _ = gr.Component.SetValue(gr.Key, gr.OriginalValue) + }() + + rebuilt, err := gr.Component.Rebuild() + if err != nil { + return false, "", errors.Wrap(err, "could not rebuild request") + } + + gologger.Verbose().Msgf("[%s] Replaying with payload for %s context: %s", a.Name(), reflection.Context, rebuilt.String()) + + resp, err := options.HttpClient.Do(rebuilt) + if err != nil { + return false, "", errors.Wrap(err, "could not send replay request") + } + defer resp.Body.Close() + + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return false, "", errors.Wrap(err, "could not read replay response body") + } + + respBodyStr := string(respBody) + + // Check if the payload is reflected unencoded + if strings.Contains(respBodyStr, payload) { + details := fmt.Sprintf( + "[xss_context] XSS confirmed in %s context (tag: %s, param: %s). Payload reflected unencoded: %s (original: %s)", + reflection.Context, + reflection.TagName, + gr.Key, + payload, + origPayload, + ) + + if hasCSP(options.ResponseHeaders) { + details += " [note: CSP header present, may limit exploitability]" + } + + return true, details, nil + } + + return false, "", nil +} + +// isHTMLResponse checks if the Content-Type indicates an HTML response +func isHTMLResponse(headers map[string][]string) bool { + if headers == nil { + return true // assume HTML if no headers available + } + ct := getHeader(headers, "Content-Type") + if ct == "" { + return true // assume HTML if no Content-Type + } + ctLower := strings.ToLower(ct) + return strings.Contains(ctLower, "text/html") || strings.Contains(ctLower, "application/xhtml") +} + +// hasCSP checks if Content-Security-Policy header is present +func hasCSP(headers map[string][]string) bool { + if headers == nil { + return false + } + return getHeader(headers, "Content-Security-Policy") != "" +} + +// getHeader gets the first value for a header (case-insensitive) +func getHeader(headers map[string][]string, name string) string { + // http.Header is already canonical, use direct lookup first + if vals, ok := headers[http.CanonicalHeaderKey(name)]; ok && len(vals) > 0 { + return vals[0] + } + // Fallback: case-insensitive search + nameLower := strings.ToLower(name) + for k, vals := range headers { + if strings.ToLower(k) == nameLower && len(vals) > 0 { + return vals[0] + } + } + return "" +} + +// detectCharacterSurvival checks which XSS-critical characters survived server-side encoding +func detectCharacterSurvival(body string, canary string) CharacterSet { + return CharacterSet{ + LessThan: strings.Contains(body, canary+"<"), + GreaterThan: strings.Contains(body, canary+"<>") || strings.Contains(body, canary+">"), + DoubleQuote: strings.Contains(body, canary+`<>"`), + SingleQuote: strings.Contains(body, canary+`<>"'`), + ForwardSlash: strings.Contains(body, canary+canaryChars), // full canary+chars survived + } +} + +// selectPayloads returns context-appropriate XSS payloads filtered by character availability +func selectPayloads(reflection *ReflectionInfo, chars CharacterSet) []string { + var candidates []string + + switch reflection.Context { + case ContextHTMLText: + if chars.LessThan && chars.GreaterThan { + candidates = []string{ + ``, + ``, + `
`, + } + } + + case ContextAttribute: + if reflection.QuoteChar == '"' && chars.DoubleQuote { + candidates = []string{ + `" onfocus=alert(1) autofocus="`, + `" onmouseover=alert(1) "`, + `">
`, + } + } else if reflection.QuoteChar == '\'' && chars.SingleQuote { + candidates = []string{ + `' onfocus=alert(1) autofocus='`, + `' onmouseover=alert(1) '`, + `'>`, + } + } + // If angle brackets available, try tag breakout even without matching quotes + if len(candidates) == 0 && chars.LessThan && chars.GreaterThan { + candidates = []string{ + `">`, + `'>`, + } + } + + case ContextAttributeUnquoted: + candidates = []string{ + ` onfocus=alert(1) autofocus`, + ` onmouseover=alert(1)`, + } + if chars.LessThan && chars.GreaterThan { + candidates = append(candidates, `>`) + } + + case ContextScript: + candidates = []string{ + ``, + `;alert(1)//`, + `\nalert(1)//`, + } + + case ContextScriptString: + if chars.SingleQuote { + candidates = append(candidates, `';alert(1)//`) + } + if chars.DoubleQuote { + candidates = append(candidates, `";alert(1)//`) + } + if chars.LessThan && chars.GreaterThan { + candidates = append(candidates, ``) + } + if len(candidates) == 0 { + candidates = []string{``} + } + + case ContextStyle: + candidates = []string{ + ``, + } + + case ContextHTMLComment: + candidates = []string{ + `-->`, + } + } + + return candidates +} diff --git a/pkg/fuzz/analyzers/xss/context.go b/pkg/fuzz/analyzers/xss/context.go new file mode 100644 index 0000000000..cb4ee972dd --- /dev/null +++ b/pkg/fuzz/analyzers/xss/context.go @@ -0,0 +1,391 @@ +package xss + +import ( + "strings" + + "golang.org/x/net/html" +) + +// DetectReflections parses the HTML body and returns all reflection contexts +// where the marker is found. +func DetectReflections(body string, marker string) []ReflectionInfo { + if !strings.Contains(body, marker) { + return nil + } + + var reflections []ReflectionInfo + markerLower := strings.ToLower(marker) + + tokenizer := html.NewTokenizer(strings.NewReader(body)) + + var tagStack []string + inScript := false + inStyle := false + inRCDATA := false + + for { + tt := tokenizer.Next() + if tt == html.ErrorToken { + break + } + + switch tt { + case html.StartTagToken, html.SelfClosingTagToken: + // Capture raw token text before consuming attributes + rawToken := string(tokenizer.Raw()) + + tn, hasAttr := tokenizer.TagName() + tagName := string(tn) + tagNameLower := strings.ToLower(tagName) + + if tt == html.StartTagToken { + tagStack = append(tagStack, tagNameLower) + } + + switch tagNameLower { + case "script": + inScript = true + case "style": + inStyle = true + default: + if _, ok := rcdataElements[tagNameLower]; ok { + inRCDATA = true + } + } + + // Check if marker is reflected in the tag name itself + if strings.Contains(strings.ToLower(tagName), markerLower) { + reflections = append(reflections, ReflectionInfo{ + Context: ContextHTMLText, + TagName: tagNameLower, + }) + } + + // Check attributes + if hasAttr { + for { + key, val, moreAttr := tokenizer.TagAttr() + attrName := strings.ToLower(string(key)) + attrVal := string(val) + + // Check if marker is in the attribute value + if strings.Contains(strings.ToLower(attrVal), markerLower) { + ctx := ContextAttribute + + // Detect quoting style by looking at raw token text. + // If we can't resolve it precisely, keep analyzer behavior conservative + // by defaulting to double-quoted attribute handling at the call site. + quote, unquoted := detectAttrQuoting(rawToken, attrName) + if unquoted { + ctx = ContextAttributeUnquoted + } else if quote == 0 { + quote = '"' + } + + // Check for javascript: URI in URL-context attributes - treat as script context + if attrName == "href" || attrName == "src" || attrName == "action" || attrName == "formaction" || attrName == "data" || attrName == "cite" || attrName == "poster" { + if strings.HasPrefix(strings.ToLower(strings.TrimSpace(attrVal)), "javascript:") { + ctx = ContextScript + } + } + + // Check for srcdoc attribute - allows HTML injection + if attrName == "srcdoc" { + ctx = ContextHTMLText + } + + // Event handler attributes contain JavaScript + if isEventHandler(attrName) { + ctx = ContextScript + } + + reflections = append(reflections, ReflectionInfo{ + Context: ctx, + AttrName: attrName, + QuoteChar: quote, + TagName: tagNameLower, + }) + } + + // Check if marker is in the attribute name + if strings.Contains(attrName, markerLower) { + reflections = append(reflections, ReflectionInfo{ + Context: ContextHTMLText, + TagName: tagNameLower, + }) + } + + if !moreAttr { + break + } + } + } + + case html.EndTagToken: + tn, _ := tokenizer.TagName() + tagNameLower := strings.ToLower(string(tn)) + + switch tagNameLower { + case "script": + inScript = false + case "style": + inStyle = false + default: + if _, ok := rcdataElements[tagNameLower]; ok { + inRCDATA = false + } + } + + // Pop tag stack + for i := len(tagStack) - 1; i >= 0; i-- { + if tagStack[i] == tagNameLower { + tagStack = tagStack[:i] + break + } + } + + case html.TextToken: + text := string(tokenizer.Text()) + if !strings.Contains(strings.ToLower(text), markerLower) { + continue + } + + if inScript { + ctx := detectScriptStringContext(text, marker) + parentTag := "script" + if len(tagStack) > 0 { + parentTag = tagStack[len(tagStack)-1] + } + reflections = append(reflections, ReflectionInfo{ + Context: ctx, + TagName: parentTag, + }) + } else if inStyle { + reflections = append(reflections, ReflectionInfo{ + Context: ContextStyle, + TagName: "style", + }) + } else if inRCDATA { + tag := "" + if len(tagStack) > 0 { + tag = tagStack[len(tagStack)-1] + } + reflections = append(reflections, ReflectionInfo{ + Context: ContextHTMLText, + TagName: tag, + }) + } else { + tag := "" + if len(tagStack) > 0 { + tag = tagStack[len(tagStack)-1] + } + reflections = append(reflections, ReflectionInfo{ + Context: ContextHTMLText, + TagName: tag, + }) + } + + case html.CommentToken: + text := string(tokenizer.Text()) + if strings.Contains(strings.ToLower(text), markerLower) { + reflections = append(reflections, ReflectionInfo{ + Context: ContextHTMLComment, + }) + } + } + } + + return reflections +} + +// detectScriptStringContext determines if the marker is inside a JS string literal +// or in bare script code. +func detectScriptStringContext(scriptContent, marker string) Context { + markerLower := strings.ToLower(marker) + contentLower := strings.ToLower(scriptContent) + + idx := strings.Index(contentLower, markerLower) + if idx < 0 { + return ContextScript + } + + // Walk through the script content tracking quote state. + // For template literals, track ${...} expression blocks separately, because + // marker inside ${...} is JS expression context, not string-literal context. + inSingleQuote := false + inDoubleQuote := false + inBacktick := false + inTemplateExpr := false + templateExprDepth := 0 + escaped := false + + for i := 0; i < idx; i++ { + ch := scriptContent[i] + + if escaped { + escaped = false + continue + } + + if inSingleQuote { + if ch == '\\' { + escaped = true + continue + } + if ch == '\'' { + inSingleQuote = false + } + continue + } + + if inDoubleQuote { + if ch == '\\' { + escaped = true + continue + } + if ch == '"' { + inDoubleQuote = false + } + continue + } + + if inBacktick { + if inTemplateExpr { + switch ch { + case '{': + templateExprDepth++ + case '}': + templateExprDepth-- + if templateExprDepth == 0 { + inTemplateExpr = false + } + case '\'': + inSingleQuote = true + case '"': + inDoubleQuote = true + case '`': + inBacktick = true + } + continue + } + + if ch == '\\' { + escaped = true + continue + } + + if ch == '$' && i+1 < idx && scriptContent[i+1] == '{' { + inTemplateExpr = true + templateExprDepth = 1 + i++ + continue + } + + if ch == '`' { + inBacktick = false + } + continue + } + + switch ch { + case '\'': + inSingleQuote = true + case '"': + inDoubleQuote = true + case '`': + inBacktick = true + } + } + + if inSingleQuote || inDoubleQuote || (inBacktick && !inTemplateExpr) { + return ContextScriptString + } + return ContextScript +} + +// detectAttrQuoting detects the quoting style of an attribute from raw HTML. +// Returns the quote character and whether the attribute is unquoted. +// If the exact attribute assignment cannot be resolved, it returns (0, false). +func detectAttrQuoting(rawToken, attrName string) (byte, bool) { + rawLower := strings.ToLower(rawToken) + attrLower := strings.ToLower(attrName) + + searchFrom := 0 + for { + rel := strings.Index(rawLower[searchFrom:], attrLower) + if rel < 0 { + return 0, false + } + idx := searchFrom + rel + + // Ensure we matched a full attribute name boundary (not a suffix/prefix of another token) + if idx > 0 { + prev := rawLower[idx-1] + if isAttrNameChar(prev) { + searchFrom = idx + len(attrLower) + continue + } + } + afterName := idx + len(attrLower) + if afterName < len(rawLower) { + next := rawLower[afterName] + if isAttrNameChar(next) { + searchFrom = idx + len(attrLower) + continue + } + } + + pos := afterName + for pos < len(rawToken) && isHTMLSpace(rawToken[pos]) { + pos++ + } + if pos >= len(rawToken) || rawToken[pos] != '=' { + searchFrom = idx + len(attrLower) + continue + } + pos++ + for pos < len(rawToken) && isHTMLSpace(rawToken[pos]) { + pos++ + } + if pos >= len(rawToken) { + return 0, false + } + + switch rawToken[pos] { + case '"': + return '"', false + case '\'': + return '\'', false + default: + return 0, true + } + } +} + +func isAttrNameChar(ch byte) bool { + return (ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9') || ch == '-' || ch == '_' || ch == ':' +} + +func isHTMLSpace(ch byte) bool { + switch ch { + case ' ', '\t', '\n', '\r', '\f': + return true + default: + return false + } +} + +// BestReflection returns the highest-priority reflection from the list +func BestReflection(reflections []ReflectionInfo) *ReflectionInfo { + if len(reflections) == 0 { + return nil + } + + best := &reflections[0] + for i := 1; i < len(reflections); i++ { + if reflections[i].Context.priority() > best.Context.priority() { + best = &reflections[i] + } + } + return best +} diff --git a/pkg/fuzz/analyzers/xss/context_test.go b/pkg/fuzz/analyzers/xss/context_test.go new file mode 100644 index 0000000000..6efcce3d6c --- /dev/null +++ b/pkg/fuzz/analyzers/xss/context_test.go @@ -0,0 +1,604 @@ +package xss + +import ( + "fmt" + "strings" + "testing" +) + +const testMarker = "nucleiXSScanary" + +func TestDetectReflections_HTMLText(t *testing.T) { + body := `

Hello nucleiXSScanary world

` + reflections := DetectReflections(body, testMarker) + if len(reflections) == 0 { + t.Fatal("expected at least one reflection in HTML text") + } + if reflections[0].Context != ContextHTMLText { + t.Fatalf("expected ContextHTMLText, got %s", reflections[0].Context) + } +} + +func TestDetectReflections_AttributeDoubleQuoted(t *testing.T) { + body := `` + reflections := DetectReflections(body, testMarker) + if len(reflections) == 0 { + t.Fatal("expected at least one reflection in attribute") + } + found := false + for _, r := range reflections { + if r.Context == ContextAttribute && r.AttrName == "value" { + found = true + break + } + } + if !found { + t.Fatalf("expected ContextAttribute for value attr, got %v", reflections) + } +} + +func TestDetectReflections_AttributeSingleQuoted(t *testing.T) { + body := `` + reflections := DetectReflections(body, testMarker) + if len(reflections) == 0 { + t.Fatal("expected at least one reflection in single-quoted attribute") + } + found := false + for _, r := range reflections { + if r.Context == ContextAttribute && r.QuoteChar == '\'' { + found = true + break + } + } + if !found { + t.Fatalf("expected single-quoted ContextAttribute, got %v", reflections) + } +} + +func TestDetectReflections_ScriptBlock(t *testing.T) { + body := `` + reflections := DetectReflections(body, testMarker) + if len(reflections) == 0 { + t.Fatal("expected at least one reflection in script") + } + if reflections[0].Context != ContextScript { + t.Fatalf("expected ContextScript, got %s", reflections[0].Context) + } +} + +func TestDetectReflections_ScriptStringDouble(t *testing.T) { + body := `` + reflections := DetectReflections(body, testMarker) + if len(reflections) == 0 { + t.Fatal("expected at least one reflection in script string") + } + if reflections[0].Context != ContextScriptString { + t.Fatalf("expected ContextScriptString, got %s", reflections[0].Context) + } +} + +func TestDetectReflections_ScriptStringSingle(t *testing.T) { + body := `` + reflections := DetectReflections(body, testMarker) + if len(reflections) == 0 { + t.Fatal("expected at least one reflection in script string") + } + if reflections[0].Context != ContextScriptString { + t.Fatalf("expected ContextScriptString, got %s", reflections[0].Context) + } +} + +func TestDetectReflections_ScriptStringBacktick(t *testing.T) { + body := "" + reflections := DetectReflections(body, testMarker) + if len(reflections) == 0 { + t.Fatal("expected at least one reflection in template literal") + } + if reflections[0].Context != ContextScriptString { + t.Fatalf("expected ContextScriptString, got %s", reflections[0].Context) + } +} + +func TestDetectReflections_HTMLComment(t *testing.T) { + body := `` + reflections := DetectReflections(body, testMarker) + if len(reflections) == 0 { + t.Fatal("expected at least one reflection in HTML comment") + } + if reflections[0].Context != ContextHTMLComment { + t.Fatalf("expected ContextHTMLComment, got %s", reflections[0].Context) + } +} + +func TestDetectReflections_StyleBlock(t *testing.T) { + body := `` + reflections := DetectReflections(body, testMarker) + if len(reflections) == 0 { + t.Fatal("expected at least one reflection in style") + } + if reflections[0].Context != ContextStyle { + t.Fatalf("expected ContextStyle, got %s", reflections[0].Context) + } +} + +func TestDetectReflections_EventHandler(t *testing.T) { + body := `
test
` + reflections := DetectReflections(body, testMarker) + if len(reflections) == 0 { + t.Fatal("expected at least one reflection in event handler") + } + found := false + for _, r := range reflections { + if r.Context == ContextScript && r.AttrName == "onmouseover" { + found = true + break + } + } + if !found { + t.Fatalf("expected ContextScript for event handler, got %v", reflections) + } +} + +func TestDetectReflections_JavascriptURI(t *testing.T) { + body := `x` + reflections := DetectReflections(body, testMarker) + if len(reflections) == 0 { + t.Fatal("expected at least one reflection in javascript: URI") + } + foundScript := false + foundLegacyAttr := false + for _, r := range reflections { + if r.Context == ContextScript && r.AttrName == "href" { + foundScript = true + } + if r.Context == ContextAttribute && r.AttrName == "href" { + foundLegacyAttr = true + } + } + if !foundScript { + t.Fatalf("expected ContextScript for javascript: href, got %v", reflections) + } + if foundLegacyAttr { + t.Fatalf("unexpected ContextAttribute for javascript: href, got %v", reflections) + } +} + +func TestDetectReflections_Srcdoc(t *testing.T) { + body := `` + reflections := DetectReflections(body, testMarker) + if len(reflections) == 0 { + t.Fatal("expected at least one reflection in srcdoc attribute") + } + foundHTMLText := false + foundLegacyAttr := false + for _, r := range reflections { + if r.Context == ContextHTMLText && r.AttrName == "srcdoc" { + foundHTMLText = true + } + if r.Context == ContextAttribute && r.AttrName == "srcdoc" { + foundLegacyAttr = true + } + } + if !foundHTMLText { + t.Fatalf("expected ContextHTMLText for srcdoc, got %v", reflections) + } + if foundLegacyAttr { + t.Fatalf("unexpected ContextAttribute for srcdoc, got %v", reflections) + } +} + +func TestDetectReflections_NoReflection(t *testing.T) { + body := `

Hello world

` + reflections := DetectReflections(body, testMarker) + if len(reflections) != 0 { + t.Fatalf("expected no reflections, got %d", len(reflections)) + } +} + +func TestDetectReflections_NoReflection_EncodedMarker(t *testing.T) { + body := `

%6e%75%63%6c%65%69%58%53%53%63%61%6e%61%72%79 nucleiXSScanary

` + reflections := DetectReflections(body, testMarker) + if len(reflections) != 0 { + t.Fatalf("expected no reflections for encoded marker, got %d", len(reflections)) + } +} + +func TestDetectReflections_MultipleContexts(t *testing.T) { + body := ` +

nucleiXSScanary

+ + + ` + reflections := DetectReflections(body, testMarker) + if len(reflections) < 3 { + t.Fatalf("expected at least 3 reflections, got %d", len(reflections)) + } + + contexts := make(map[Context]bool) + for _, r := range reflections { + contexts[r.Context] = true + } + if !contexts[ContextHTMLText] { + t.Error("expected ContextHTMLText in multiple reflections") + } + if !contexts[ContextAttribute] { + t.Error("expected ContextAttribute in multiple reflections") + } + if !contexts[ContextScriptString] { + t.Error("expected ContextScriptString in multiple reflections") + } +} + +func TestDetectReflections_Textarea(t *testing.T) { + body := `` + reflections := DetectReflections(body, testMarker) + if len(reflections) == 0 { + t.Fatal("expected reflection in textarea (RCDATA element)") + } + if reflections[0].Context != ContextHTMLText { + t.Fatalf("expected ContextHTMLText for RCDATA element, got %s", reflections[0].Context) + } +} + +func TestDetectReflections_Title(t *testing.T) { + body := `nucleiXSScanary` + reflections := DetectReflections(body, testMarker) + if len(reflections) == 0 { + t.Fatal("expected reflection in title element") + } +} + +func TestDetectReflections_Helper_CaseInsensitive(t *testing.T) { + body := `

NUCLEIxssCANARY

` + reflections := DetectReflections(body, "nucleixsscanary") + if len(reflections) == 0 { + t.Fatal("expected helper-level case-insensitive reflection detection") + } +} + +func TestDetectReflections_TagNameReflection(t *testing.T) { + body := `test` + reflections := DetectReflections(body, testMarker) + if len(reflections) == 0 { + t.Fatal("expected reflection in tag name") + } +} + +func TestBestReflection_Priority(t *testing.T) { + reflections := []ReflectionInfo{ + {Context: ContextHTMLComment}, + {Context: ContextHTMLText}, + {Context: ContextAttribute}, + {Context: ContextScript}, + } + best := BestReflection(reflections) + if best == nil || best.Context != ContextScript { + t.Fatal("expected ContextScript as highest priority") + } +} + +func TestBestReflection_Empty(t *testing.T) { + best := BestReflection(nil) + if best != nil { + t.Fatal("expected nil for empty reflections") + } +} + +func TestDetectScriptStringContext_NotInString(t *testing.T) { + content := `var x = nucleiXSScanary;` + ctx := detectScriptStringContext(content, testMarker) + if ctx != ContextScript { + t.Fatalf("expected ContextScript, got %s", ctx) + } +} + +func TestDetectScriptStringContext_InDoubleQuote(t *testing.T) { + content := `var x = "nucleiXSScanary";` + ctx := detectScriptStringContext(content, testMarker) + if ctx != ContextScriptString { + t.Fatalf("expected ContextScriptString, got %s", ctx) + } +} + +func TestDetectScriptStringContext_InSingleQuote(t *testing.T) { + content := `var x = 'nucleiXSScanary';` + ctx := detectScriptStringContext(content, testMarker) + if ctx != ContextScriptString { + t.Fatalf("expected ContextScriptString, got %s", ctx) + } +} + +func TestDetectScriptStringContext_EscapedQuote(t *testing.T) { + content := `var x = "test\"nucleiXSScanary";` + ctx := detectScriptStringContext(content, testMarker) + if ctx != ContextScriptString { + t.Fatalf("expected ContextScriptString for escaped quote, got %s", ctx) + } +} + +func TestDetectScriptStringContext_AfterClosedString(t *testing.T) { + content := `var x = "test"; var y = nucleiXSScanary;` + ctx := detectScriptStringContext(content, testMarker) + if ctx != ContextScript { + t.Fatalf("expected ContextScript after closed string, got %s", ctx) + } +} + +func TestDetectScriptStringContext_TemplateLiteralText(t *testing.T) { + content := "const t = `hello nucleiXSScanary`;" + ctx := detectScriptStringContext(content, testMarker) + if ctx != ContextScriptString { + t.Fatalf("expected ContextScriptString in template text, got %s", ctx) + } +} + +func TestDetectScriptStringContext_TemplateInterpolation(t *testing.T) { + content := "const t = `prefix ${nucleiXSScanary}`;" + ctx := detectScriptStringContext(content, testMarker) + if ctx != ContextScript { + t.Fatalf("expected ContextScript in template interpolation, got %s", ctx) + } +} + +func TestDetectScriptStringContext_TemplateInterpolationNested(t *testing.T) { + content := "const t = `prefix ${fn({a: nucleiXSScanary})}`;" + ctx := detectScriptStringContext(content, testMarker) + if ctx != ContextScript { + t.Fatalf("expected ContextScript in nested template interpolation, got %s", ctx) + } +} + +func TestDetectAttrQuoting_WithWhitespace(t *testing.T) { + quote, unquoted := detectAttrQuoting(``, "class") + if unquoted || quote != '"' { + t.Fatalf("expected double-quoted attr with whitespace, got quote=%q unquoted=%v", quote, unquoted) + } +} + +func TestDetectAttrQuoting_UnquotedWithWhitespace(t *testing.T) { + quote, unquoted := detectAttrQuoting(``, "class") + if !unquoted || quote != 0 { + t.Fatalf("expected unquoted attr with whitespace, got quote=%q unquoted=%v", quote, unquoted) + } +} + +func TestDetectAttrQuoting_PartialMatch_DataClass(t *testing.T) { + quote, unquoted := detectAttrQuoting(``, "class") + if quote == '"' || unquoted { + t.Fatalf("expected no match for data-class partial name, got quote=%q unquoted=%v", quote, unquoted) + } +} + +func TestDetectAttrQuoting_PartialMatch_Classname(t *testing.T) { + quote, unquoted := detectAttrQuoting(``, "class") + if quote == '"' || unquoted { + t.Fatalf("expected no match for classname partial name, got quote=%q unquoted=%v", quote, unquoted) + } +} + +func TestIsEventHandler(t *testing.T) { + tests := []struct { + name string + expected bool + }{ + {"onclick", true}, + {"onload", true}, + {"onerror", true}, + {"onmouseover", true}, + {"ONCLICK", true}, + {"OnClick", true}, + {"class", false}, + {"href", false}, + {"style", false}, + {"data-onclick", false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := isEventHandler(tt.name); got != tt.expected { + t.Errorf("isEventHandler(%q) = %v, want %v", tt.name, got, tt.expected) + } + }) + } +} + +func TestContextString(t *testing.T) { + tests := []struct { + ctx Context + expected string + }{ + {ContextNone, "none"}, + {ContextHTMLComment, "html_comment"}, + {ContextHTMLText, "html_text"}, + {ContextAttribute, "attribute"}, + {ContextAttributeUnquoted, "attribute_unquoted"}, + {ContextScript, "script"}, + {ContextScriptString, "script_string"}, + {ContextStyle, "style"}, + } + for _, tt := range tests { + t.Run(tt.expected, func(t *testing.T) { + if got := tt.ctx.String(); got != tt.expected { + t.Errorf("Context.String() = %q, want %q", got, tt.expected) + } + }) + } +} + +func TestSelectPayloads(t *testing.T) { + tests := []struct { + name string + reflection ReflectionInfo + chars CharacterSet + wantEmpty bool + }{ + { + name: "HTML text with angle brackets", + reflection: ReflectionInfo{Context: ContextHTMLText}, + chars: CharacterSet{LessThan: true, GreaterThan: true}, + wantEmpty: false, + }, + { + name: "HTML text without angle brackets", + reflection: ReflectionInfo{Context: ContextHTMLText}, + chars: CharacterSet{}, + wantEmpty: true, + }, + { + name: "Double-quoted attribute with quotes", + reflection: ReflectionInfo{Context: ContextAttribute, QuoteChar: '"'}, + chars: CharacterSet{DoubleQuote: true}, + wantEmpty: false, + }, + { + name: "Script context", + reflection: ReflectionInfo{Context: ContextScript}, + chars: CharacterSet{}, + wantEmpty: false, + }, + { + name: "Script string with single quote", + reflection: ReflectionInfo{Context: ContextScriptString}, + chars: CharacterSet{SingleQuote: true}, + wantEmpty: false, + }, + { + name: "Comment context", + reflection: ReflectionInfo{Context: ContextHTMLComment}, + chars: CharacterSet{}, + wantEmpty: false, + }, + { + name: "Style context", + reflection: ReflectionInfo{Context: ContextStyle}, + chars: CharacterSet{}, + wantEmpty: false, + }, + { + name: "Unquoted attribute", + reflection: ReflectionInfo{Context: ContextAttributeUnquoted}, + chars: CharacterSet{}, + wantEmpty: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + payloads := selectPayloads(&tt.reflection, tt.chars) + if tt.wantEmpty && len(payloads) > 0 { + t.Errorf("expected no payloads, got %d", len(payloads)) + } + if !tt.wantEmpty && len(payloads) == 0 { + t.Error("expected payloads but got none") + } + }) + } +} + +func TestDetectCharacterSurvival(t *testing.T) { + canary := "testcanary" + body := canary + `<>"'/` + chars := detectCharacterSurvival(body, canary) + if !chars.LessThan { + t.Error("expected LessThan to survive") + } + if !chars.GreaterThan { + t.Error("expected GreaterThan to survive") + } + if !chars.DoubleQuote { + t.Error("expected DoubleQuote to survive") + } + if !chars.SingleQuote { + t.Error("expected SingleQuote to survive") + } + if !chars.ForwardSlash { + t.Error("expected ForwardSlash to survive") + } +} + +func TestDetectCharacterSurvival_Encoded(t *testing.T) { + canary := "testcanary" + body := canary + `<>"'/` + chars := detectCharacterSurvival(body, canary) + if chars.LessThan { + t.Error("expected LessThan to be encoded") + } + if chars.GreaterThan { + t.Error("expected GreaterThan to be encoded") + } +} + +func TestIsHTMLResponse(t *testing.T) { + tests := []struct { + name string + headers map[string][]string + expected bool + }{ + {"nil headers", nil, true}, + {"empty headers", map[string][]string{}, true}, + {"text/html", map[string][]string{"Content-Type": {"text/html; charset=utf-8"}}, true}, + {"application/xhtml", map[string][]string{"Content-Type": {"application/xhtml+xml"}}, true}, + {"application/json", map[string][]string{"Content-Type": {"application/json"}}, false}, + {"text/plain", map[string][]string{"Content-Type": {"text/plain"}}, false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := isHTMLResponse(tt.headers); got != tt.expected { + t.Errorf("isHTMLResponse() = %v, want %v", got, tt.expected) + } + }) + } +} + +func TestHasCSP(t *testing.T) { + tests := []struct { + name string + headers map[string][]string + expected bool + }{ + {"no CSP", map[string][]string{}, false}, + {"has CSP", map[string][]string{"Content-Security-Policy": {"default-src 'self'"}}, true}, + {"nil headers", nil, false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := hasCSP(tt.headers); got != tt.expected { + t.Errorf("hasCSP() = %v, want %v", got, tt.expected) + } + }) + } +} + +func BenchmarkDetectReflections(b *testing.B) { + var sb strings.Builder + sb.WriteString("") + for i := 0; i < 100; i++ { + sb.WriteString(fmt.Sprintf("
Content %d
", i, i)) + } + sb.WriteString("

nucleiXSScanary

") + sb.WriteString("") + body := sb.String() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + DetectReflections(body, testMarker) + } +} + +func BenchmarkDetectReflections_LargeBody(b *testing.B) { + var sb strings.Builder + sb.WriteString("") + for i := 0; i < 1000; i++ { + sb.WriteString(fmt.Sprintf(`
Link %dData %d
`, i, i, i, i)) + } + sb.WriteString(``) + sb.WriteString("") + body := sb.String() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + DetectReflections(body, testMarker) + } +} diff --git a/pkg/fuzz/analyzers/xss/types.go b/pkg/fuzz/analyzers/xss/types.go new file mode 100644 index 0000000000..f817d2f90f --- /dev/null +++ b/pkg/fuzz/analyzers/xss/types.go @@ -0,0 +1,206 @@ +package xss + +import "strings" + +// Context represents the HTML context where a reflection occurs +type Context int + +const ( + // ContextNone means the marker was not found in the response + ContextNone Context = iota + // ContextHTMLComment means the marker is inside an HTML comment + ContextHTMLComment + // ContextHTMLText means the marker is inside HTML text content + ContextHTMLText + // ContextAttribute means the marker is inside a quoted HTML attribute + ContextAttribute + // ContextAttributeUnquoted means the marker is inside an unquoted HTML attribute + ContextAttributeUnquoted + // ContextScript means the marker is inside a script block + ContextScript + // ContextScriptString means the marker is inside a string literal within a script block + ContextScriptString + // ContextStyle means the marker is inside a style block + ContextStyle +) + +// String returns the string representation of the context +func (c Context) String() string { + switch c { + case ContextNone: + return "none" + case ContextHTMLComment: + return "html_comment" + case ContextHTMLText: + return "html_text" + case ContextAttribute: + return "attribute" + case ContextAttributeUnquoted: + return "attribute_unquoted" + case ContextScript: + return "script" + case ContextScriptString: + return "script_string" + case ContextStyle: + return "style" + default: + return "unknown" + } +} + +// priority returns the priority of the context for choosing the best one. +// Higher value = more interesting for exploitation. +func (c Context) priority() int { + switch c { + case ContextScript, ContextScriptString: + return 5 + case ContextAttributeUnquoted: + return 4 + case ContextAttribute: + return 3 + case ContextHTMLText: + return 2 + case ContextStyle: + return 1 + case ContextHTMLComment: + return 0 + default: + return -1 + } +} + +// ReflectionInfo holds information about a detected reflection +type ReflectionInfo struct { + Context Context + AttrName string // attribute name if in attribute context + QuoteChar byte // quote character if in attribute context (' or ") + TagName string // parent tag name +} + +// CharacterSet tracks which XSS-critical characters survived encoding +type CharacterSet struct { + LessThan bool // < + GreaterThan bool // > + DoubleQuote bool // " + SingleQuote bool // ' + ForwardSlash bool // / +} + +// canaryChars are the characters appended to the canary to check survival +const canaryChars = `<>"'/` + +// eventHandlers is a set of known HTML event handler attribute names +var eventHandlers = map[string]struct{}{ + "onabort": {}, + "onafterprint": {}, + "onanimationend": {}, + "onanimationiteration": {}, + "onanimationstart": {}, + "onauxclick": {}, + "onbeforecopy": {}, + "onbeforecut": {}, + "onbeforeinput": {}, + "onbeforepaste": {}, + "onbeforeprint": {}, + "onbeforeunload": {}, + "onblur": {}, + "oncanplay": {}, + "oncanplaythrough": {}, + "onchange": {}, + "onclick": {}, + "onclose": {}, + "oncontextmenu": {}, + "oncopy": {}, + "oncuechange": {}, + "oncut": {}, + "ondblclick": {}, + "ondrag": {}, + "ondragend": {}, + "ondragenter": {}, + "ondragleave": {}, + "ondragover": {}, + "ondragstart": {}, + "ondrop": {}, + "ondurationchange": {}, + "onemptied": {}, + "onended": {}, + "onerror": {}, + "onfocus": {}, + "onfocusin": {}, + "onfocusout": {}, + "onfullscreenchange": {}, + "ongotpointercapture": {}, + "onhashchange": {}, + "oninput": {}, + "oninvalid": {}, + "onkeydown": {}, + "onkeypress": {}, + "onkeyup": {}, + "onload": {}, + "onloadeddata": {}, + "onloadedmetadata": {}, + "onloadstart": {}, + "onmessage": {}, + "onmousedown": {}, + "onmouseenter": {}, + "onmouseleave": {}, + "onmousemove": {}, + "onmouseout": {}, + "onmouseover": {}, + "onmouseup": {}, + "onmousewheel": {}, + "onoffline": {}, + "ononline": {}, + "onpagehide": {}, + "onpageshow": {}, + "onpaste": {}, + "onpause": {}, + "onplay": {}, + "onplaying": {}, + "onpointerdown": {}, + "onpointerenter": {}, + "onpointerleave": {}, + "onpointermove": {}, + "onpointerout": {}, + "onpointerover": {}, + "onpointerup": {}, + "onpopstate": {}, + "onprogress": {}, + "onratechange": {}, + "onreset": {}, + "onresize": {}, + "onscroll": {}, + "onsearch": {}, + "onseeked": {}, + "onseeking": {}, + "onselect": {}, + "onstalled": {}, + "onstorage": {}, + "onsubmit": {}, + "onsuspend": {}, + "ontimeupdate": {}, + "ontoggle": {}, + "ontouchcancel": {}, + "ontouchend": {}, + "ontouchmove": {}, + "ontouchstart": {}, + "ontransitionend": {}, + "onunload": {}, + "onvolumechange": {}, + "onwaiting": {}, + "onwheel": {}, +} + +// isEventHandler returns true if the attribute name is a known event handler +func isEventHandler(name string) bool { + _, ok := eventHandlers[strings.ToLower(name)] + return ok +} + +// rcdataElements are HTML elements whose content is treated as RCDATA (no tag parsing) +var rcdataElements = map[string]struct{}{ + "textarea": {}, + "title": {}, + "xmp": {}, + "noscript": {}, +} diff --git a/pkg/protocols/http/http.go b/pkg/protocols/http/http.go index d461db5920..e985421a48 100644 --- a/pkg/protocols/http/http.go +++ b/pkg/protocols/http/http.go @@ -1,551 +1,552 @@ -package http - -import ( - "bytes" - "fmt" - "math" - "strings" - "time" - - "github.com/invopop/jsonschema" - json "github.com/json-iterator/go" - "github.com/pkg/errors" - - "github.com/projectdiscovery/fastdialer/fastdialer" - _ "github.com/projectdiscovery/nuclei/v3/pkg/fuzz/analyzers/time" - - "github.com/projectdiscovery/nuclei/v3/pkg/fuzz" - "github.com/projectdiscovery/nuclei/v3/pkg/fuzz/analyzers" - "github.com/projectdiscovery/nuclei/v3/pkg/operators" - "github.com/projectdiscovery/nuclei/v3/pkg/operators/matchers" - "github.com/projectdiscovery/nuclei/v3/pkg/protocols" - "github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/expressions" - "github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/generators" - "github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate" - "github.com/projectdiscovery/nuclei/v3/pkg/protocols/http/httpclientpool" - "github.com/projectdiscovery/nuclei/v3/pkg/protocols/network/networkclientpool" - httputil "github.com/projectdiscovery/nuclei/v3/pkg/protocols/utils/http" - "github.com/projectdiscovery/nuclei/v3/pkg/utils/stats" - "github.com/projectdiscovery/rawhttp" - "github.com/projectdiscovery/retryablehttp-go" - fileutil "github.com/projectdiscovery/utils/file" -) - -// Request contains a http request to be made from a template -type Request struct { - // Operators for the current request go here. - operators.Operators `yaml:",inline" json:",inline"` - // description: | - // Path contains the path/s for the HTTP requests. It supports variables - // as placeholders. - // examples: - // - name: Some example path values - // value: > - // []string{"{{BaseURL}}", "{{BaseURL}}/+CSCOU+/../+CSCOE+/files/file_list.json?path=/sessions"} - Path []string `yaml:"path,omitempty" json:"path,omitempty" jsonschema:"title=path(s) for the http request,description=Path(s) to send http requests to"` - // description: | - // Raw contains HTTP Requests in Raw format. - // examples: - // - name: Some example raw requests - // value: | - // []string{"GET /etc/passwd HTTP/1.1\nHost:\nContent-Length: 4", "POST /.%0d./.%0d./.%0d./.%0d./bin/sh HTTP/1.1\nHost: {{Hostname}}\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:71.0) Gecko/20100101 Firefox/71.0\nContent-Length: 1\nConnection: close\n\necho\necho\ncat /etc/passwd 2>&1"} - Raw []string `yaml:"raw,omitempty" json:"raw,omitempty" jsonschema:"http requests in raw format,description=HTTP Requests in Raw Format"` - // ID is the optional id of the request - ID string `yaml:"id,omitempty" json:"id,omitempty" jsonschema:"title=id for the http request,description=ID for the HTTP Request"` - // description: | - // Name is the optional name of the request. - // - // If a name is specified, all the named request in a template can be matched upon - // in a combined manner allowing multi-request based matchers. - Name string `yaml:"name,omitempty" json:"name,omitempty" jsonschema:"title=name for the http request,description=Optional name for the HTTP Request"` - // description: | - // Attack is the type of payload combinations to perform. - // - // batteringram is inserts the same payload into all defined payload positions at once, pitchfork combines multiple payload sets and clusterbomb generates - // permutations and combinations for all payloads. - // values: - // - "batteringram" - // - "pitchfork" - // - "clusterbomb" - AttackType generators.AttackTypeHolder `yaml:"attack,omitempty" json:"attack,omitempty" jsonschema:"title=attack is the payload combination,description=Attack is the type of payload combinations to perform,enum=batteringram,enum=pitchfork,enum=clusterbomb"` - // description: | - // Method is the HTTP Request Method. - Method HTTPMethodTypeHolder `yaml:"method,omitempty" json:"method,omitempty" jsonschema:"title=method is the http request method,description=Method is the HTTP Request Method,enum=GET,enum=HEAD,enum=POST,enum=PUT,enum=DELETE,enum=CONNECT,enum=OPTIONS,enum=TRACE,enum=PATCH,enum=PURGE"` - // description: | - // Body is an optional parameter which contains HTTP Request body. - // examples: - // - name: Same Body for a Login POST request - // value: "\"username=test&password=test\"" - Body string `yaml:"body,omitempty" json:"body,omitempty" jsonschema:"title=body is the http request body,description=Body is an optional parameter which contains HTTP Request body"` - // description: | - // Payloads contains any payloads for the current request. - // - // Payloads support both key-values combinations where a list - // of payloads is provided, or optionally a single file can also - // be provided as payload which will be read on run-time. - Payloads map[string]interface{} `yaml:"payloads,omitempty" json:"payloads,omitempty" jsonschema:"title=payloads for the http request,description=Payloads contains any payloads for the current request"` - - // description: | - // Headers contains HTTP Headers to send with the request. - // examples: - // - value: | - // map[string]string{"Content-Type": "application/x-www-form-urlencoded", "Content-Length": "1", "Any-Header": "Any-Value"} - Headers map[string]string `yaml:"headers,omitempty" json:"headers,omitempty" jsonschema:"title=headers to send with the http request,description=Headers contains HTTP Headers to send with the request"` - // description: | - // RaceCount is the number of times to send a request in Race Condition Attack. - // examples: - // - name: Send a request 5 times - // value: "5" - RaceNumberRequests int `yaml:"race_count,omitempty" json:"race_count,omitempty" jsonschema:"title=number of times to repeat request in race condition,description=Number of times to send a request in Race Condition Attack"` - // description: | - // MaxRedirects is the maximum number of redirects that should be followed. - // examples: - // - name: Follow up to 5 redirects - // value: "5" - MaxRedirects int `yaml:"max-redirects,omitempty" json:"max-redirects,omitempty" jsonschema:"title=maximum number of redirects to follow,description=Maximum number of redirects that should be followed"` - // description: | - // PipelineConcurrentConnections is number of connections to create during pipelining. - // examples: - // - name: Create 40 concurrent connections - // value: 40 - PipelineConcurrentConnections int `yaml:"pipeline-concurrent-connections,omitempty" json:"pipeline-concurrent-connections,omitempty" jsonschema:"title=number of pipelining connections,description=Number of connections to create during pipelining"` - // description: | - // PipelineRequestsPerConnection is number of requests to send per connection when pipelining. - // examples: - // - name: Send 100 requests per pipeline connection - // value: 100 - PipelineRequestsPerConnection int `yaml:"pipeline-requests-per-connection,omitempty" json:"pipeline-requests-per-connection,omitempty" jsonschema:"title=number of requests to send per pipelining connections,description=Number of requests to send per connection when pipelining"` - // description: | - // Threads specifies number of threads to use sending requests. This enables Connection Pooling. - // - // Connection: Close attribute must not be used in request while using threads flag, otherwise - // pooling will fail and engine will continue to close connections after requests. - // examples: - // - name: Send requests using 10 concurrent threads - // value: 10 - Threads int `yaml:"threads,omitempty" json:"threads,omitempty" jsonschema:"title=threads for sending requests,description=Threads specifies number of threads to use sending requests. This enables Connection Pooling"` - // description: | - // MaxSize is the maximum size of http response body to read in bytes. - // examples: - // - name: Read max 2048 bytes of the response - // value: 2048 - MaxSize int `yaml:"max-size,omitempty" json:"max-size,omitempty" jsonschema:"title=maximum http response body size,description=Maximum size of http response body to read in bytes"` - - // Fuzzing describes schema to fuzz http requests - Fuzzing []*fuzz.Rule `yaml:"fuzzing,omitempty" json:"fuzzing,omitempty" jsonschema:"title=fuzzin rules for http fuzzing,description=Fuzzing describes rule schema to fuzz http requests"` - // description: | - // Analyzer is an analyzer to use for matching the response. - Analyzer *analyzers.AnalyzerTemplate `yaml:"analyzer,omitempty" json:"analyzer,omitempty" jsonschema:"title=analyzer for http request,description=Analyzer for HTTP Request"` - - CompiledOperators *operators.Operators `yaml:"-" json:"-"` - - options *protocols.ExecutorOptions - connConfiguration *httpclientpool.Configuration - totalRequests int - customHeaders map[string]string - generator *generators.PayloadGenerator // optional, only enabled when using payloads - httpClient *retryablehttp.Client - rawhttpClient *rawhttp.Client - dialer *fastdialer.Dialer - - // description: | - // SelfContained specifies if the request is self-contained. - SelfContained bool `yaml:"self-contained,omitempty" json:"self-contained,omitempty"` - - // description: | - // Signature is the request signature method - // values: - // - "AWS" - Signature SignatureTypeHolder `yaml:"signature,omitempty" json:"signature,omitempty" jsonschema:"title=signature is the http request signature method,description=Signature is the HTTP Request signature Method,enum=AWS"` - - // description: | - // SkipSecretFile skips the authentication or authorization configured in the secret file. - SkipSecretFile bool `yaml:"skip-secret-file,omitempty" json:"skip-secret-file,omitempty" jsonschema:"title=bypass secret file,description=Skips the authentication or authorization configured in the secret file"` - - // description: | - // CookieReuse is an optional setting that enables cookie reuse for - // all requests defined in raw section. - // Deprecated: This is default now. Use disable-cookie to disable cookie reuse. cookie-reuse will be removed in future releases. - CookieReuse bool `yaml:"cookie-reuse,omitempty" json:"cookie-reuse,omitempty" jsonschema:"title=optional cookie reuse enable,description=Optional setting that enables cookie reuse"` - - // description: | - // DisableCookie is an optional setting that disables cookie reuse - DisableCookie bool `yaml:"disable-cookie,omitempty" json:"disable-cookie,omitempty" jsonschema:"title=optional disable cookie reuse,description=Optional setting that disables cookie reuse"` - - // description: | - // Enables force reading of the entire raw unsafe request body ignoring - // any specified content length headers. - ForceReadAllBody bool `yaml:"read-all,omitempty" json:"read-all,omitempty" jsonschema:"title=force read all body,description=Enables force reading of entire unsafe http request body"` - // description: | - // Redirects specifies whether redirects should be followed by the HTTP Client. - // - // This can be used in conjunction with `max-redirects` to control the HTTP request redirects. - Redirects bool `yaml:"redirects,omitempty" json:"redirects,omitempty" jsonschema:"title=follow http redirects,description=Specifies whether redirects should be followed by the HTTP Client"` - // description: | - // Redirects specifies whether only redirects to the same host should be followed by the HTTP Client. - // - // This can be used in conjunction with `max-redirects` to control the HTTP request redirects. - HostRedirects bool `yaml:"host-redirects,omitempty" json:"host-redirects,omitempty" jsonschema:"title=follow same host http redirects,description=Specifies whether redirects to the same host should be followed by the HTTP Client"` - // description: | - // Pipeline defines if the attack should be performed with HTTP 1.1 Pipelining - // - // All requests must be idempotent (GET/POST). This can be used for race conditions/billions requests. - Pipeline bool `yaml:"pipeline,omitempty" json:"pipeline,omitempty" jsonschema:"title=perform HTTP 1.1 pipelining,description=Pipeline defines if the attack should be performed with HTTP 1.1 Pipelining"` - // description: | - // Unsafe specifies whether to use rawhttp engine for sending Non RFC-Compliant requests. - // - // This uses the [rawhttp](https://github.com/projectdiscovery/rawhttp) engine to achieve complete - // control over the request, with no normalization performed by the client. - Unsafe bool `yaml:"unsafe,omitempty" json:"unsafe,omitempty" jsonschema:"title=use rawhttp non-strict-rfc client,description=Unsafe specifies whether to use rawhttp engine for sending Non RFC-Compliant requests"` - // description: | - // Race determines if all the request have to be attempted at the same time (Race Condition) - // - // The actual number of requests that will be sent is determined by the `race_count` field. - Race bool `yaml:"race,omitempty" json:"race,omitempty" jsonschema:"title=perform race-http request coordination attack,description=Race determines if all the request have to be attempted at the same time (Race Condition)"` - // description: | - // ReqCondition automatically assigns numbers to requests and preserves their history. - // - // This allows matching on them later for multi-request conditions. - // Deprecated: request condition will be detected automatically (https://github.com/projectdiscovery/nuclei/issues/2393) - ReqCondition bool `yaml:"req-condition,omitempty" json:"req-condition,omitempty" jsonschema:"title=preserve request history,description=Automatically assigns numbers to requests and preserves their history"` - // 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" json:"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" json:"skip-variables-check,omitempty" jsonschema:"title=skip variable checks,description=Skips the check for unresolved variables in request"` - // description: | - // IterateAll iterates all the values extracted from internal extractors - // Deprecated: Use flow instead . iterate-all will be removed in future releases - IterateAll bool `yaml:"iterate-all,omitempty" json:"iterate-all,omitempty" jsonschema:"title=iterate all the values,description=Iterates all the values extracted from internal extractors"` - // description: | - // DigestAuthUsername specifies the username for digest authentication - DigestAuthUsername string `yaml:"digest-username,omitempty" json:"digest-username,omitempty" jsonschema:"title=specifies the username for digest authentication,description=Optional parameter which specifies the username for digest auth"` - // description: | - // DigestAuthPassword specifies the password for digest authentication - DigestAuthPassword string `yaml:"digest-password,omitempty" json:"digest-password,omitempty" jsonschema:"title=specifies the password for digest authentication,description=Optional parameter which specifies the password for digest auth"` - // description: | - // DisablePathAutomerge disables merging target url path with raw request path - DisablePathAutomerge bool `yaml:"disable-path-automerge,omitempty" json:"disable-path-automerge,omitempty" jsonschema:"title=disable auto merging of path,description=Disable merging target url path with raw request path"` - // description: | - // Fuzz PreCondition is matcher-like field to check if fuzzing should be performed on this request or not - FuzzPreCondition []*matchers.Matcher `yaml:"pre-condition,omitempty" json:"pre-condition,omitempty" jsonschema:"title=pre-condition for fuzzing/dast,description=PreCondition is matcher-like field to check if fuzzing should be performed on this request or not"` - // description: | - // FuzzPreConditionOperator is the operator between multiple PreConditions for fuzzing Default is OR - FuzzPreConditionOperator string `yaml:"pre-condition-operator,omitempty" json:"pre-condition-operator,omitempty" jsonschema:"title=condition between the filters,description=Operator to use between multiple per-conditions,enum=and,enum=or"` - fuzzPreConditionOperator matchers.ConditionType `yaml:"-" json:"-"` - // description: | - // GlobalMatchers marks matchers as static and applies globally to all result events from other templates - GlobalMatchers bool `yaml:"global-matchers,omitempty" json:"global-matchers,omitempty" jsonschema:"title=global matchers,description=marks matchers as static and applies globally to all result events from other templates"` -} - -func (e Request) JSONSchemaExtend(schema *jsonschema.Schema) { - headersSchema, ok := schema.Properties.Get("headers") - if !ok { - return - } - headersSchema.PatternProperties = map[string]*jsonschema.Schema{ - ".*": { - OneOf: []*jsonschema.Schema{ - { - Type: "string", - }, - { - Type: "integer", - }, - { - Type: "boolean", - }, - }, - }, - } - headersSchema.Ref = "" -} - -// Options returns executer options for http request -func (r *Request) Options() *protocols.ExecutorOptions { - return r.options -} - -// RequestPartDefinitions contains a mapping of request part definitions and their -// description. Multiple definitions are separated by commas. -// Definitions not having a name (generated on runtime) are prefixed & suffixed by <>. -var RequestPartDefinitions = map[string]string{ - "template-id": "ID of the template executed", - "template-info": "Info Block of the template executed", - "template-path": "Path of the template executed", - "host": "Host is the input to the template", - "matched": "Matched is the input which was matched upon", - "type": "Type is the type of request made", - "request": "HTTP request made from the client", - "response": "HTTP response received from server", - "status_code": "Status Code received from the Server", - "body": "HTTP response body received from server (default)", - "content_length": "HTTP Response content length", - "header,all_headers": "HTTP response headers", - "duration": "HTTP request time duration", - "all": "HTTP response body + headers", - "cookies_from_response": "HTTP response cookies in name:value format", - "headers_from_response": "HTTP response headers in name:value format", -} - -// GetID returns the unique ID of the request if any. -func (request *Request) GetID() string { - return request.ID -} - -func (request *Request) isRaw() bool { - return len(request.Raw) > 0 -} - -// Compile compiles the protocol request for further execution. -func (request *Request) Compile(options *protocols.ExecutorOptions) error { - if err := request.validate(); err != nil { - return errors.Wrap(err, "validation error") - } - - connectionConfiguration := &httpclientpool.Configuration{ - Threads: request.Threads, - MaxRedirects: request.MaxRedirects, - NoTimeout: false, - DisableCookie: request.DisableCookie, - Connection: &httpclientpool.ConnectionConfiguration{ - DisableKeepAlive: httputil.ShouldDisableKeepAlive(options.Options), - }, - RedirectFlow: httpclientpool.DontFollowRedirect, - } - var customTimeout int - if request.Analyzer != nil && request.Analyzer.Name == "time_delay" { - var timeoutVal int - if timeout, ok := request.Analyzer.Parameters["sleep_duration"]; ok { - timeoutVal, _ = timeout.(int) - } else { - timeoutVal = 5 - } - - // Add 5x buffer to the timeout - customTimeout = int(math.Ceil(float64(timeoutVal) * 5)) - } - if customTimeout > 0 { - connectionConfiguration.Connection.CustomMaxTimeout = time.Duration(customTimeout) * time.Second - } - - if request.Redirects || options.Options.FollowRedirects { - connectionConfiguration.RedirectFlow = httpclientpool.FollowAllRedirect - } - if request.HostRedirects || options.Options.FollowHostRedirects { - connectionConfiguration.RedirectFlow = httpclientpool.FollowSameHostRedirect - } - - // If we have request level timeout, ignore http client timeouts - for _, req := range request.Raw { - if reTimeoutAnnotation.MatchString(req) { - connectionConfiguration.NoTimeout = true - } - } - request.connConfiguration = connectionConfiguration - - client, err := httpclientpool.Get(options.Options, connectionConfiguration) - if err != nil { - return errors.Wrap(err, "could not get dns client") - } - request.customHeaders = make(map[string]string) - request.httpClient = client - - dialer, err := networkclientpool.Get(options.Options, &networkclientpool.Configuration{ - CustomDialer: options.CustomFastdialer, - }) - if err != nil { - return errors.Wrap(err, "could not get dialer") - } - request.dialer = dialer - - request.options = options - for _, option := range request.options.Options.CustomHeaders { - parts := strings.SplitN(option, ":", 2) - if len(parts) != 2 { - continue - } - request.customHeaders[parts[0]] = strings.TrimSpace(parts[1]) - } - - if request.Body != "" && !strings.Contains(request.Body, "\r\n") { - request.Body = strings.ReplaceAll(request.Body, "\n", "\r\n") - } - if len(request.Raw) > 0 { - for i, raw := range request.Raw { - if !strings.Contains(raw, "\r\n") { - request.Raw[i] = strings.ReplaceAll(raw, "\n", "\r\n") - } - } - request.rawhttpClient = httpclientpool.GetRawHTTP(options) - } - if len(request.Matchers) > 0 || len(request.Extractors) > 0 { - compiled := &request.Operators - compiled.ExcludeMatchers = options.ExcludeMatchers - compiled.TemplateID = options.TemplateID - if compileErr := compiled.Compile(); compileErr != nil { - return errors.Wrap(compileErr, "could not compile operators") - } - request.CompiledOperators = compiled - } - - // === fuzzing filters ===== // - - if request.FuzzPreConditionOperator != "" { - request.fuzzPreConditionOperator = matchers.ConditionTypes[request.FuzzPreConditionOperator] - } else { - request.fuzzPreConditionOperator = matchers.ORCondition - } - - for _, filter := range request.FuzzPreCondition { - if err := filter.CompileMatchers(); err != nil { - return errors.Wrap(err, "could not compile matcher") - } - } - - if request.Analyzer != nil { - if analyzer := analyzers.GetAnalyzer(request.Analyzer.Name); analyzer == nil { - return errors.Errorf("analyzer %s not found", request.Analyzer.Name) - } - } - - // Resolve payload paths from vars if they exists - for name, payload := range request.options.Options.Vars.AsMap() { - payloadStr, ok := payload.(string) - // check if inputs contains the payload - var hasPayloadName bool - // search for markers in all request parts - var inputs []string - inputs = append(inputs, request.Method.String(), request.Body) - inputs = append(inputs, request.Raw...) - for k, v := range request.customHeaders { - inputs = append(inputs, fmt.Sprintf("%s: %s", k, v)) - } - for k, v := range request.Headers { - inputs = append(inputs, fmt.Sprintf("%s: %s", k, v)) - } - - for _, input := range inputs { - if expressions.ContainsVariablesWithNames(map[string]interface{}{name: payload}, input) == nil { - hasPayloadName = true - break - } - } - if ok && hasPayloadName && fileutil.FileExists(payloadStr) { - if request.Payloads == nil { - request.Payloads = make(map[string]interface{}) - } - request.Payloads[name] = payloadStr - } - } - - // tries to drop unused payloads - by marshaling sections that might contain the payload - unusedPayloads := make(map[string]struct{}) - requestSectionsToCheck := []interface{}{ - request.customHeaders, request.Headers, request.Matchers, - request.Extractors, request.Body, request.Path, request.Raw, request.Fuzzing, - } - if requestSectionsToCheckData, err := json.Marshal(requestSectionsToCheck); err == nil { - for payload := range request.Payloads { - if bytes.Contains(requestSectionsToCheckData, []byte(payload)) { - continue - } - unusedPayloads[payload] = struct{}{} - } - } - for payload := range unusedPayloads { - delete(request.Payloads, payload) - } - - if len(request.Payloads) > 0 { - request.generator, err = generators.New(request.Payloads, request.AttackType.Value, request.options.TemplatePath, request.options.Catalog, request.options.Options.AttackType, request.options.Options) - if err != nil { - return errors.Wrap(err, "could not parse payloads") - } - } - request.options = options - request.totalRequests = request.Requests() - - if len(request.Fuzzing) > 0 { - if request.Unsafe { - return errors.New("cannot use unsafe with http fuzzing templates") - } - for _, rule := range request.Fuzzing { - if fuzzingMode := options.Options.FuzzingMode; fuzzingMode != "" { - rule.Mode = fuzzingMode - } - if fuzzingType := options.Options.FuzzingType; fuzzingType != "" { - rule.Type = fuzzingType - } - if err := rule.Compile(request.generator, request.options); err != nil { - return errors.Wrap(err, "could not compile fuzzing rule") - } - } - } - if len(request.Payloads) > 0 { - // Due to a known issue (https://github.com/projectdiscovery/nuclei/issues/5015), - // dynamic extractors cannot be used with payloads. To address this, - // execution is handled by the standard engine without concurrency, - // achieved by setting the thread count to 0. - - // this limitation will be removed once we have a better way to handle dynamic extractors with payloads - hasMultipleRequests := false - if len(request.Raw)+len(request.Path) > 1 { - hasMultipleRequests = true - } - // look for dynamic extractor ( internal: true with named extractor) - hasNamedInternalExtractor := false - for _, extractor := range request.Extractors { - if extractor.Internal && extractor.Name != "" { - hasNamedInternalExtractor = true - break - } - } - if hasNamedInternalExtractor && hasMultipleRequests { - stats.Increment(SetThreadToCountZero) - request.Threads = 0 - } else { - // specifically for http requests high concurrency and threads will lead to memory exhaustion, hence reduce the maximum parallelism - if protocolstate.IsLowOnMemory() { - request.Threads = protocolstate.GuardThreadsOrDefault(request.Threads) - } - request.Threads = options.GetThreadsForNPayloadRequests(request.Requests(), request.Threads) - } - } - return nil -} - -// RebuildGenerator rebuilds the generator for the request -func (request *Request) RebuildGenerator() error { - generator, err := generators.New(request.Payloads, request.AttackType.Value, request.options.TemplatePath, request.options.Catalog, request.options.Options.AttackType, request.options.Options) - if err != nil { - return errors.Wrap(err, "could not parse payloads") - } - request.generator = generator - return nil -} - -// Requests returns the total number of requests the YAML rule will perform -func (request *Request) Requests() int { - generator := request.newGenerator(false) - return generator.Total() -} - -const ( - SetThreadToCountZero = "set-thread-count-to-zero" -) - -func init() { - stats.NewEntry(SetThreadToCountZero, "Setting thread count to 0 for %d templates, dynamic extractors are not supported with payloads yet") -} - -// UpdateOptions replaces this request's options with a new copy -func (r *Request) UpdateOptions(opts *protocols.ExecutorOptions) { - r.options.ApplyNewEngineOptions(opts) -} - -// HasFuzzing indicates whether the request has fuzzing rules defined. -func (request *Request) HasFuzzing() bool { - return len(request.Fuzzing) > 0 -} +package http + +import ( + "bytes" + "fmt" + "math" + "strings" + "time" + + "github.com/invopop/jsonschema" + json "github.com/json-iterator/go" + "github.com/pkg/errors" + + "github.com/projectdiscovery/fastdialer/fastdialer" + _ "github.com/projectdiscovery/nuclei/v3/pkg/fuzz/analyzers/time" + _ "github.com/projectdiscovery/nuclei/v3/pkg/fuzz/analyzers/xss" + + "github.com/projectdiscovery/nuclei/v3/pkg/fuzz" + "github.com/projectdiscovery/nuclei/v3/pkg/fuzz/analyzers" + "github.com/projectdiscovery/nuclei/v3/pkg/operators" + "github.com/projectdiscovery/nuclei/v3/pkg/operators/matchers" + "github.com/projectdiscovery/nuclei/v3/pkg/protocols" + "github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/expressions" + "github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/generators" + "github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate" + "github.com/projectdiscovery/nuclei/v3/pkg/protocols/http/httpclientpool" + "github.com/projectdiscovery/nuclei/v3/pkg/protocols/network/networkclientpool" + httputil "github.com/projectdiscovery/nuclei/v3/pkg/protocols/utils/http" + "github.com/projectdiscovery/nuclei/v3/pkg/utils/stats" + "github.com/projectdiscovery/rawhttp" + "github.com/projectdiscovery/retryablehttp-go" + fileutil "github.com/projectdiscovery/utils/file" +) + +// Request contains a http request to be made from a template +type Request struct { + // Operators for the current request go here. + operators.Operators `yaml:",inline" json:",inline"` + // description: | + // Path contains the path/s for the HTTP requests. It supports variables + // as placeholders. + // examples: + // - name: Some example path values + // value: > + // []string{"{{BaseURL}}", "{{BaseURL}}/+CSCOU+/../+CSCOE+/files/file_list.json?path=/sessions"} + Path []string `yaml:"path,omitempty" json:"path,omitempty" jsonschema:"title=path(s) for the http request,description=Path(s) to send http requests to"` + // description: | + // Raw contains HTTP Requests in Raw format. + // examples: + // - name: Some example raw requests + // value: | + // []string{"GET /etc/passwd HTTP/1.1\nHost:\nContent-Length: 4", "POST /.%0d./.%0d./.%0d./.%0d./bin/sh HTTP/1.1\nHost: {{Hostname}}\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:71.0) Gecko/20100101 Firefox/71.0\nContent-Length: 1\nConnection: close\n\necho\necho\ncat /etc/passwd 2>&1"} + Raw []string `yaml:"raw,omitempty" json:"raw,omitempty" jsonschema:"http requests in raw format,description=HTTP Requests in Raw Format"` + // ID is the optional id of the request + ID string `yaml:"id,omitempty" json:"id,omitempty" jsonschema:"title=id for the http request,description=ID for the HTTP Request"` + // description: | + // Name is the optional name of the request. + // + // If a name is specified, all the named request in a template can be matched upon + // in a combined manner allowing multi-request based matchers. + Name string `yaml:"name,omitempty" json:"name,omitempty" jsonschema:"title=name for the http request,description=Optional name for the HTTP Request"` + // description: | + // Attack is the type of payload combinations to perform. + // + // batteringram is inserts the same payload into all defined payload positions at once, pitchfork combines multiple payload sets and clusterbomb generates + // permutations and combinations for all payloads. + // values: + // - "batteringram" + // - "pitchfork" + // - "clusterbomb" + AttackType generators.AttackTypeHolder `yaml:"attack,omitempty" json:"attack,omitempty" jsonschema:"title=attack is the payload combination,description=Attack is the type of payload combinations to perform,enum=batteringram,enum=pitchfork,enum=clusterbomb"` + // description: | + // Method is the HTTP Request Method. + Method HTTPMethodTypeHolder `yaml:"method,omitempty" json:"method,omitempty" jsonschema:"title=method is the http request method,description=Method is the HTTP Request Method,enum=GET,enum=HEAD,enum=POST,enum=PUT,enum=DELETE,enum=CONNECT,enum=OPTIONS,enum=TRACE,enum=PATCH,enum=PURGE"` + // description: | + // Body is an optional parameter which contains HTTP Request body. + // examples: + // - name: Same Body for a Login POST request + // value: "\"username=test&password=test\"" + Body string `yaml:"body,omitempty" json:"body,omitempty" jsonschema:"title=body is the http request body,description=Body is an optional parameter which contains HTTP Request body"` + // description: | + // Payloads contains any payloads for the current request. + // + // Payloads support both key-values combinations where a list + // of payloads is provided, or optionally a single file can also + // be provided as payload which will be read on run-time. + Payloads map[string]interface{} `yaml:"payloads,omitempty" json:"payloads,omitempty" jsonschema:"title=payloads for the http request,description=Payloads contains any payloads for the current request"` + + // description: | + // Headers contains HTTP Headers to send with the request. + // examples: + // - value: | + // map[string]string{"Content-Type": "application/x-www-form-urlencoded", "Content-Length": "1", "Any-Header": "Any-Value"} + Headers map[string]string `yaml:"headers,omitempty" json:"headers,omitempty" jsonschema:"title=headers to send with the http request,description=Headers contains HTTP Headers to send with the request"` + // description: | + // RaceCount is the number of times to send a request in Race Condition Attack. + // examples: + // - name: Send a request 5 times + // value: "5" + RaceNumberRequests int `yaml:"race_count,omitempty" json:"race_count,omitempty" jsonschema:"title=number of times to repeat request in race condition,description=Number of times to send a request in Race Condition Attack"` + // description: | + // MaxRedirects is the maximum number of redirects that should be followed. + // examples: + // - name: Follow up to 5 redirects + // value: "5" + MaxRedirects int `yaml:"max-redirects,omitempty" json:"max-redirects,omitempty" jsonschema:"title=maximum number of redirects to follow,description=Maximum number of redirects that should be followed"` + // description: | + // PipelineConcurrentConnections is number of connections to create during pipelining. + // examples: + // - name: Create 40 concurrent connections + // value: 40 + PipelineConcurrentConnections int `yaml:"pipeline-concurrent-connections,omitempty" json:"pipeline-concurrent-connections,omitempty" jsonschema:"title=number of pipelining connections,description=Number of connections to create during pipelining"` + // description: | + // PipelineRequestsPerConnection is number of requests to send per connection when pipelining. + // examples: + // - name: Send 100 requests per pipeline connection + // value: 100 + PipelineRequestsPerConnection int `yaml:"pipeline-requests-per-connection,omitempty" json:"pipeline-requests-per-connection,omitempty" jsonschema:"title=number of requests to send per pipelining connections,description=Number of requests to send per connection when pipelining"` + // description: | + // Threads specifies number of threads to use sending requests. This enables Connection Pooling. + // + // Connection: Close attribute must not be used in request while using threads flag, otherwise + // pooling will fail and engine will continue to close connections after requests. + // examples: + // - name: Send requests using 10 concurrent threads + // value: 10 + Threads int `yaml:"threads,omitempty" json:"threads,omitempty" jsonschema:"title=threads for sending requests,description=Threads specifies number of threads to use sending requests. This enables Connection Pooling"` + // description: | + // MaxSize is the maximum size of http response body to read in bytes. + // examples: + // - name: Read max 2048 bytes of the response + // value: 2048 + MaxSize int `yaml:"max-size,omitempty" json:"max-size,omitempty" jsonschema:"title=maximum http response body size,description=Maximum size of http response body to read in bytes"` + + // Fuzzing describes schema to fuzz http requests + Fuzzing []*fuzz.Rule `yaml:"fuzzing,omitempty" json:"fuzzing,omitempty" jsonschema:"title=fuzzin rules for http fuzzing,description=Fuzzing describes rule schema to fuzz http requests"` + // description: | + // Analyzer is an analyzer to use for matching the response. + Analyzer *analyzers.AnalyzerTemplate `yaml:"analyzer,omitempty" json:"analyzer,omitempty" jsonschema:"title=analyzer for http request,description=Analyzer for HTTP Request"` + + CompiledOperators *operators.Operators `yaml:"-" json:"-"` + + options *protocols.ExecutorOptions + connConfiguration *httpclientpool.Configuration + totalRequests int + customHeaders map[string]string + generator *generators.PayloadGenerator // optional, only enabled when using payloads + httpClient *retryablehttp.Client + rawhttpClient *rawhttp.Client + dialer *fastdialer.Dialer + + // description: | + // SelfContained specifies if the request is self-contained. + SelfContained bool `yaml:"self-contained,omitempty" json:"self-contained,omitempty"` + + // description: | + // Signature is the request signature method + // values: + // - "AWS" + Signature SignatureTypeHolder `yaml:"signature,omitempty" json:"signature,omitempty" jsonschema:"title=signature is the http request signature method,description=Signature is the HTTP Request signature Method,enum=AWS"` + + // description: | + // SkipSecretFile skips the authentication or authorization configured in the secret file. + SkipSecretFile bool `yaml:"skip-secret-file,omitempty" json:"skip-secret-file,omitempty" jsonschema:"title=bypass secret file,description=Skips the authentication or authorization configured in the secret file"` + + // description: | + // CookieReuse is an optional setting that enables cookie reuse for + // all requests defined in raw section. + // Deprecated: This is default now. Use disable-cookie to disable cookie reuse. cookie-reuse will be removed in future releases. + CookieReuse bool `yaml:"cookie-reuse,omitempty" json:"cookie-reuse,omitempty" jsonschema:"title=optional cookie reuse enable,description=Optional setting that enables cookie reuse"` + + // description: | + // DisableCookie is an optional setting that disables cookie reuse + DisableCookie bool `yaml:"disable-cookie,omitempty" json:"disable-cookie,omitempty" jsonschema:"title=optional disable cookie reuse,description=Optional setting that disables cookie reuse"` + + // description: | + // Enables force reading of the entire raw unsafe request body ignoring + // any specified content length headers. + ForceReadAllBody bool `yaml:"read-all,omitempty" json:"read-all,omitempty" jsonschema:"title=force read all body,description=Enables force reading of entire unsafe http request body"` + // description: | + // Redirects specifies whether redirects should be followed by the HTTP Client. + // + // This can be used in conjunction with `max-redirects` to control the HTTP request redirects. + Redirects bool `yaml:"redirects,omitempty" json:"redirects,omitempty" jsonschema:"title=follow http redirects,description=Specifies whether redirects should be followed by the HTTP Client"` + // description: | + // Redirects specifies whether only redirects to the same host should be followed by the HTTP Client. + // + // This can be used in conjunction with `max-redirects` to control the HTTP request redirects. + HostRedirects bool `yaml:"host-redirects,omitempty" json:"host-redirects,omitempty" jsonschema:"title=follow same host http redirects,description=Specifies whether redirects to the same host should be followed by the HTTP Client"` + // description: | + // Pipeline defines if the attack should be performed with HTTP 1.1 Pipelining + // + // All requests must be idempotent (GET/POST). This can be used for race conditions/billions requests. + Pipeline bool `yaml:"pipeline,omitempty" json:"pipeline,omitempty" jsonschema:"title=perform HTTP 1.1 pipelining,description=Pipeline defines if the attack should be performed with HTTP 1.1 Pipelining"` + // description: | + // Unsafe specifies whether to use rawhttp engine for sending Non RFC-Compliant requests. + // + // This uses the [rawhttp](https://github.com/projectdiscovery/rawhttp) engine to achieve complete + // control over the request, with no normalization performed by the client. + Unsafe bool `yaml:"unsafe,omitempty" json:"unsafe,omitempty" jsonschema:"title=use rawhttp non-strict-rfc client,description=Unsafe specifies whether to use rawhttp engine for sending Non RFC-Compliant requests"` + // description: | + // Race determines if all the request have to be attempted at the same time (Race Condition) + // + // The actual number of requests that will be sent is determined by the `race_count` field. + Race bool `yaml:"race,omitempty" json:"race,omitempty" jsonschema:"title=perform race-http request coordination attack,description=Race determines if all the request have to be attempted at the same time (Race Condition)"` + // description: | + // ReqCondition automatically assigns numbers to requests and preserves their history. + // + // This allows matching on them later for multi-request conditions. + // Deprecated: request condition will be detected automatically (https://github.com/projectdiscovery/nuclei/issues/2393) + ReqCondition bool `yaml:"req-condition,omitempty" json:"req-condition,omitempty" jsonschema:"title=preserve request history,description=Automatically assigns numbers to requests and preserves their history"` + // 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" json:"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" json:"skip-variables-check,omitempty" jsonschema:"title=skip variable checks,description=Skips the check for unresolved variables in request"` + // description: | + // IterateAll iterates all the values extracted from internal extractors + // Deprecated: Use flow instead . iterate-all will be removed in future releases + IterateAll bool `yaml:"iterate-all,omitempty" json:"iterate-all,omitempty" jsonschema:"title=iterate all the values,description=Iterates all the values extracted from internal extractors"` + // description: | + // DigestAuthUsername specifies the username for digest authentication + DigestAuthUsername string `yaml:"digest-username,omitempty" json:"digest-username,omitempty" jsonschema:"title=specifies the username for digest authentication,description=Optional parameter which specifies the username for digest auth"` + // description: | + // DigestAuthPassword specifies the password for digest authentication + DigestAuthPassword string `yaml:"digest-password,omitempty" json:"digest-password,omitempty" jsonschema:"title=specifies the password for digest authentication,description=Optional parameter which specifies the password for digest auth"` + // description: | + // DisablePathAutomerge disables merging target url path with raw request path + DisablePathAutomerge bool `yaml:"disable-path-automerge,omitempty" json:"disable-path-automerge,omitempty" jsonschema:"title=disable auto merging of path,description=Disable merging target url path with raw request path"` + // description: | + // Fuzz PreCondition is matcher-like field to check if fuzzing should be performed on this request or not + FuzzPreCondition []*matchers.Matcher `yaml:"pre-condition,omitempty" json:"pre-condition,omitempty" jsonschema:"title=pre-condition for fuzzing/dast,description=PreCondition is matcher-like field to check if fuzzing should be performed on this request or not"` + // description: | + // FuzzPreConditionOperator is the operator between multiple PreConditions for fuzzing Default is OR + FuzzPreConditionOperator string `yaml:"pre-condition-operator,omitempty" json:"pre-condition-operator,omitempty" jsonschema:"title=condition between the filters,description=Operator to use between multiple per-conditions,enum=and,enum=or"` + fuzzPreConditionOperator matchers.ConditionType `yaml:"-" json:"-"` + // description: | + // GlobalMatchers marks matchers as static and applies globally to all result events from other templates + GlobalMatchers bool `yaml:"global-matchers,omitempty" json:"global-matchers,omitempty" jsonschema:"title=global matchers,description=marks matchers as static and applies globally to all result events from other templates"` +} + +func (e Request) JSONSchemaExtend(schema *jsonschema.Schema) { + headersSchema, ok := schema.Properties.Get("headers") + if !ok { + return + } + headersSchema.PatternProperties = map[string]*jsonschema.Schema{ + ".*": { + OneOf: []*jsonschema.Schema{ + { + Type: "string", + }, + { + Type: "integer", + }, + { + Type: "boolean", + }, + }, + }, + } + headersSchema.Ref = "" +} + +// Options returns executer options for http request +func (r *Request) Options() *protocols.ExecutorOptions { + return r.options +} + +// RequestPartDefinitions contains a mapping of request part definitions and their +// description. Multiple definitions are separated by commas. +// Definitions not having a name (generated on runtime) are prefixed & suffixed by <>. +var RequestPartDefinitions = map[string]string{ + "template-id": "ID of the template executed", + "template-info": "Info Block of the template executed", + "template-path": "Path of the template executed", + "host": "Host is the input to the template", + "matched": "Matched is the input which was matched upon", + "type": "Type is the type of request made", + "request": "HTTP request made from the client", + "response": "HTTP response received from server", + "status_code": "Status Code received from the Server", + "body": "HTTP response body received from server (default)", + "content_length": "HTTP Response content length", + "header,all_headers": "HTTP response headers", + "duration": "HTTP request time duration", + "all": "HTTP response body + headers", + "cookies_from_response": "HTTP response cookies in name:value format", + "headers_from_response": "HTTP response headers in name:value format", +} + +// GetID returns the unique ID of the request if any. +func (request *Request) GetID() string { + return request.ID +} + +func (request *Request) isRaw() bool { + return len(request.Raw) > 0 +} + +// Compile compiles the protocol request for further execution. +func (request *Request) Compile(options *protocols.ExecutorOptions) error { + if err := request.validate(); err != nil { + return errors.Wrap(err, "validation error") + } + + connectionConfiguration := &httpclientpool.Configuration{ + Threads: request.Threads, + MaxRedirects: request.MaxRedirects, + NoTimeout: false, + DisableCookie: request.DisableCookie, + Connection: &httpclientpool.ConnectionConfiguration{ + DisableKeepAlive: httputil.ShouldDisableKeepAlive(options.Options), + }, + RedirectFlow: httpclientpool.DontFollowRedirect, + } + var customTimeout int + if request.Analyzer != nil && request.Analyzer.Name == "time_delay" { + var timeoutVal int + if timeout, ok := request.Analyzer.Parameters["sleep_duration"]; ok { + timeoutVal, _ = timeout.(int) + } else { + timeoutVal = 5 + } + + // Add 5x buffer to the timeout + customTimeout = int(math.Ceil(float64(timeoutVal) * 5)) + } + if customTimeout > 0 { + connectionConfiguration.Connection.CustomMaxTimeout = time.Duration(customTimeout) * time.Second + } + + if request.Redirects || options.Options.FollowRedirects { + connectionConfiguration.RedirectFlow = httpclientpool.FollowAllRedirect + } + if request.HostRedirects || options.Options.FollowHostRedirects { + connectionConfiguration.RedirectFlow = httpclientpool.FollowSameHostRedirect + } + + // If we have request level timeout, ignore http client timeouts + for _, req := range request.Raw { + if reTimeoutAnnotation.MatchString(req) { + connectionConfiguration.NoTimeout = true + } + } + request.connConfiguration = connectionConfiguration + + client, err := httpclientpool.Get(options.Options, connectionConfiguration) + if err != nil { + return errors.Wrap(err, "could not get dns client") + } + request.customHeaders = make(map[string]string) + request.httpClient = client + + dialer, err := networkclientpool.Get(options.Options, &networkclientpool.Configuration{ + CustomDialer: options.CustomFastdialer, + }) + if err != nil { + return errors.Wrap(err, "could not get dialer") + } + request.dialer = dialer + + request.options = options + for _, option := range request.options.Options.CustomHeaders { + parts := strings.SplitN(option, ":", 2) + if len(parts) != 2 { + continue + } + request.customHeaders[parts[0]] = strings.TrimSpace(parts[1]) + } + + if request.Body != "" && !strings.Contains(request.Body, "\r\n") { + request.Body = strings.ReplaceAll(request.Body, "\n", "\r\n") + } + if len(request.Raw) > 0 { + for i, raw := range request.Raw { + if !strings.Contains(raw, "\r\n") { + request.Raw[i] = strings.ReplaceAll(raw, "\n", "\r\n") + } + } + request.rawhttpClient = httpclientpool.GetRawHTTP(options) + } + if len(request.Matchers) > 0 || len(request.Extractors) > 0 { + compiled := &request.Operators + compiled.ExcludeMatchers = options.ExcludeMatchers + compiled.TemplateID = options.TemplateID + if compileErr := compiled.Compile(); compileErr != nil { + return errors.Wrap(compileErr, "could not compile operators") + } + request.CompiledOperators = compiled + } + + // === fuzzing filters ===== // + + if request.FuzzPreConditionOperator != "" { + request.fuzzPreConditionOperator = matchers.ConditionTypes[request.FuzzPreConditionOperator] + } else { + request.fuzzPreConditionOperator = matchers.ORCondition + } + + for _, filter := range request.FuzzPreCondition { + if err := filter.CompileMatchers(); err != nil { + return errors.Wrap(err, "could not compile matcher") + } + } + + if request.Analyzer != nil { + if analyzer := analyzers.GetAnalyzer(request.Analyzer.Name); analyzer == nil { + return errors.Errorf("analyzer %s not found", request.Analyzer.Name) + } + } + + // Resolve payload paths from vars if they exists + for name, payload := range request.options.Options.Vars.AsMap() { + payloadStr, ok := payload.(string) + // check if inputs contains the payload + var hasPayloadName bool + // search for markers in all request parts + var inputs []string + inputs = append(inputs, request.Method.String(), request.Body) + inputs = append(inputs, request.Raw...) + for k, v := range request.customHeaders { + inputs = append(inputs, fmt.Sprintf("%s: %s", k, v)) + } + for k, v := range request.Headers { + inputs = append(inputs, fmt.Sprintf("%s: %s", k, v)) + } + + for _, input := range inputs { + if expressions.ContainsVariablesWithNames(map[string]interface{}{name: payload}, input) == nil { + hasPayloadName = true + break + } + } + if ok && hasPayloadName && fileutil.FileExists(payloadStr) { + if request.Payloads == nil { + request.Payloads = make(map[string]interface{}) + } + request.Payloads[name] = payloadStr + } + } + + // tries to drop unused payloads - by marshaling sections that might contain the payload + unusedPayloads := make(map[string]struct{}) + requestSectionsToCheck := []interface{}{ + request.customHeaders, request.Headers, request.Matchers, + request.Extractors, request.Body, request.Path, request.Raw, request.Fuzzing, + } + if requestSectionsToCheckData, err := json.Marshal(requestSectionsToCheck); err == nil { + for payload := range request.Payloads { + if bytes.Contains(requestSectionsToCheckData, []byte(payload)) { + continue + } + unusedPayloads[payload] = struct{}{} + } + } + for payload := range unusedPayloads { + delete(request.Payloads, payload) + } + + if len(request.Payloads) > 0 { + request.generator, err = generators.New(request.Payloads, request.AttackType.Value, request.options.TemplatePath, request.options.Catalog, request.options.Options.AttackType, request.options.Options) + if err != nil { + return errors.Wrap(err, "could not parse payloads") + } + } + request.options = options + request.totalRequests = request.Requests() + + if len(request.Fuzzing) > 0 { + if request.Unsafe { + return errors.New("cannot use unsafe with http fuzzing templates") + } + for _, rule := range request.Fuzzing { + if fuzzingMode := options.Options.FuzzingMode; fuzzingMode != "" { + rule.Mode = fuzzingMode + } + if fuzzingType := options.Options.FuzzingType; fuzzingType != "" { + rule.Type = fuzzingType + } + if err := rule.Compile(request.generator, request.options); err != nil { + return errors.Wrap(err, "could not compile fuzzing rule") + } + } + } + if len(request.Payloads) > 0 { + // Due to a known issue (https://github.com/projectdiscovery/nuclei/issues/5015), + // dynamic extractors cannot be used with payloads. To address this, + // execution is handled by the standard engine without concurrency, + // achieved by setting the thread count to 0. + + // this limitation will be removed once we have a better way to handle dynamic extractors with payloads + hasMultipleRequests := false + if len(request.Raw)+len(request.Path) > 1 { + hasMultipleRequests = true + } + // look for dynamic extractor ( internal: true with named extractor) + hasNamedInternalExtractor := false + for _, extractor := range request.Extractors { + if extractor.Internal && extractor.Name != "" { + hasNamedInternalExtractor = true + break + } + } + if hasNamedInternalExtractor && hasMultipleRequests { + stats.Increment(SetThreadToCountZero) + request.Threads = 0 + } else { + // specifically for http requests high concurrency and threads will lead to memory exhaustion, hence reduce the maximum parallelism + if protocolstate.IsLowOnMemory() { + request.Threads = protocolstate.GuardThreadsOrDefault(request.Threads) + } + request.Threads = options.GetThreadsForNPayloadRequests(request.Requests(), request.Threads) + } + } + return nil +} + +// RebuildGenerator rebuilds the generator for the request +func (request *Request) RebuildGenerator() error { + generator, err := generators.New(request.Payloads, request.AttackType.Value, request.options.TemplatePath, request.options.Catalog, request.options.Options.AttackType, request.options.Options) + if err != nil { + return errors.Wrap(err, "could not parse payloads") + } + request.generator = generator + return nil +} + +// Requests returns the total number of requests the YAML rule will perform +func (request *Request) Requests() int { + generator := request.newGenerator(false) + return generator.Total() +} + +const ( + SetThreadToCountZero = "set-thread-count-to-zero" +) + +func init() { + stats.NewEntry(SetThreadToCountZero, "Setting thread count to 0 for %d templates, dynamic extractors are not supported with payloads yet") +} + +// UpdateOptions replaces this request's options with a new copy +func (r *Request) UpdateOptions(opts *protocols.ExecutorOptions) { + r.options.ApplyNewEngineOptions(opts) +} + +// HasFuzzing indicates whether the request has fuzzing rules defined. +func (request *Request) HasFuzzing() bool { + return len(request.Fuzzing) > 0 +} diff --git a/pkg/protocols/http/request.go b/pkg/protocols/http/request.go index 3b08c50e14..02cef0f380 100644 --- a/pkg/protocols/http/request.go +++ b/pkg/protocols/http/request.go @@ -1012,11 +1012,23 @@ func (request *Request) executeRequest(input *contextargs.Context, generatedRequ if request.Analyzer != nil { analyzer := analyzers.GetAnalyzer(request.Analyzer.Name) + + // Extract response data for analyzer + var respHeaders map[string][]string + var respStatusCode int + if respChain.Response() != nil { + respHeaders = respChain.Response().Header + respStatusCode = respChain.Response().StatusCode + } + analysisMatched, analysisDetails, err := analyzer.Analyze(&analyzers.Options{ FuzzGenerated: generatedRequest.fuzzGeneratedRequest, HttpClient: request.httpClient, ResponseTimeDelay: duration, AnalyzerParameters: request.Analyzer.Parameters, + ResponseBody: bodyStr, + ResponseHeaders: respHeaders, + ResponseStatusCode: respStatusCode, }) if err != nil { gologger.Warning().Msgf("Could not analyze response: %v\n", err) diff --git a/pkg/protocols/http/request_fuzz.go b/pkg/protocols/http/request_fuzz.go index 3a7e2cc74a..11d6afe2a8 100644 --- a/pkg/protocols/http/request_fuzz.go +++ b/pkg/protocols/http/request_fuzz.go @@ -1,324 +1,327 @@ -package http - -// === Fuzzing Documentation (Scoped to this File) ===== -// -> request.executeFuzzingRule [iterates over payloads(+requests) and executes] -// -> request.executePayloadUsingRules [executes single payload on all rules (if more than 1)] -// -> request.executeGeneratedFuzzingRequest [execute final generated fuzzing request and get result] - -import ( - "context" - "fmt" - "net/http" - "strings" - - "github.com/pkg/errors" - "github.com/projectdiscovery/gologger" - "github.com/projectdiscovery/nuclei/v3/pkg/fuzz" - "github.com/projectdiscovery/nuclei/v3/pkg/fuzz/analyzers" - "github.com/projectdiscovery/nuclei/v3/pkg/operators" - "github.com/projectdiscovery/nuclei/v3/pkg/operators/matchers" - "github.com/projectdiscovery/nuclei/v3/pkg/output" - "github.com/projectdiscovery/nuclei/v3/pkg/protocols" - "github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs" - "github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/generators" - "github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/interactsh" - "github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/utils/vardump" - protocolutils "github.com/projectdiscovery/nuclei/v3/pkg/protocols/utils" - "github.com/projectdiscovery/nuclei/v3/pkg/types" - "github.com/projectdiscovery/retryablehttp-go" - "github.com/projectdiscovery/useragent" - urlutil "github.com/projectdiscovery/utils/url" -) - -// executeFuzzingRule executes fuzzing request for a URL -// TODO: -// 1. use SPMHandler and rewrite stop at first match logic here -// 2. use scanContext instead of contextargs.Context -func (request *Request) executeFuzzingRule(input *contextargs.Context, previous output.InternalEvent, callback protocols.OutputEventCallback) error { - // methdology: - // to check applicablity of rule, we first try to execute it with one value - // if it is applicable, we execute all requests - // if it is not applicable, we log and fail silently - - // check if target should be fuzzed or not - if !request.ShouldFuzzTarget(input) { - urlx, _ := input.MetaInput.URL() - if urlx != nil { - gologger.Verbose().Msgf("[%s] fuzz: target(%s) not applicable for fuzzing\n", request.options.TemplateID, urlx.String()) - } else { - gologger.Verbose().Msgf("[%s] fuzz: target(%s) not applicable for fuzzing\n", request.options.TemplateID, input.MetaInput.Input) - } - return nil - } - - if input.MetaInput.Input == "" && input.MetaInput.ReqResp == nil { - return errors.New("empty input provided for fuzzing") - } - - // ==== fuzzing when full HTTP request is provided ===== - - if input.MetaInput.ReqResp != nil { - baseRequest, err := input.MetaInput.ReqResp.BuildRequest() - if err != nil { - return errors.Wrap(err, "fuzz: could not build request obtained from target file") - } - request.addHeadersToRequest(baseRequest) - input.MetaInput.Input = baseRequest.String() - // execute with one value first to checks its applicability - err = request.executeAllFuzzingRules(input, previous, baseRequest, callback) - if err != nil { - // in case of any error, return it - if fuzz.IsErrRuleNotApplicable(err) { - // log and fail silently - gologger.Verbose().Msgf("[%s] fuzz: %s\n", request.options.TemplateID, err) - return nil - } - if errors.Is(err, ErrMissingVars) { - return err - } - gologger.Verbose().Msgf("[%s] fuzz: payload request execution failed: %s\n", request.options.TemplateID, err) - } - return nil - } - - // ==== fuzzing when only URL is provided ===== - - // we need to use this url instead of input - inputx := input.Clone() - parsed, err := urlutil.ParseAbsoluteURL(input.MetaInput.Input, true) - if err != nil { - return errors.Wrap(err, "fuzz: could not parse input url") - } - baseRequest, err := retryablehttp.NewRequestFromURL(http.MethodGet, parsed, nil) - if err != nil { - return errors.Wrap(err, "fuzz: could not build request from url") - } - userAgent := useragent.PickRandom() - baseRequest.Header.Set("User-Agent", userAgent.Raw) - request.addHeadersToRequest(baseRequest) - - // execute with one value first to checks its applicability - err = request.executeAllFuzzingRules(inputx, previous, baseRequest, callback) - if err != nil { - // in case of any error, return it - if fuzz.IsErrRuleNotApplicable(err) { - // log and fail silently - gologger.Verbose().Msgf("[%s] fuzz: %s\n", request.options.TemplateID, err) - return nil - } - if errors.Is(err, ErrMissingVars) { - return err - } - gologger.Verbose().Msgf("[%s] fuzz: payload request execution failed: %s\n", request.options.TemplateID, err) - } - return nil -} - -func (request *Request) addHeadersToRequest(baseRequest *retryablehttp.Request) { - for k, v := range request.Headers { - baseRequest.Header.Set(k, v) - } -} - -// executeAllFuzzingRules executes all fuzzing rules defined in template for a given base request -func (request *Request) executeAllFuzzingRules(input *contextargs.Context, values map[string]interface{}, baseRequest *retryablehttp.Request, callback protocols.OutputEventCallback) error { - applicable := false - values = generators.MergeMaps(request.filterDataMap(input), values) - for _, rule := range request.Fuzzing { - select { - case <-input.Context().Done(): - return input.Context().Err() - default: - } - - input := &fuzz.ExecuteRuleInput{ - Input: input, - DisplayFuzzPoints: request.options.Options.DisplayFuzzPoints, - Callback: func(gr fuzz.GeneratedRequest) bool { - select { - case <-input.Context().Done(): - return false - default: - } - - // TODO: replace this after scanContext Refactor - return request.executeGeneratedFuzzingRequest(gr, input, callback) - }, - Values: values, - BaseRequest: baseRequest.Clone(context.TODO()), - } - if request.Analyzer != nil { - analyzer := analyzers.GetAnalyzer(request.Analyzer.Name) - input.ApplyPayloadInitialTransformation = analyzer.ApplyInitialTransformation - input.AnalyzerParams = request.Analyzer.Parameters - } - err := rule.Execute(input) - if err == nil { - applicable = true - continue - } - if fuzz.IsErrRuleNotApplicable(err) { - gologger.Verbose().Msgf("[%s] fuzz: %s\n", request.options.TemplateID, err) - continue - } - if err == types.ErrNoMoreRequests { - return nil - } - return errors.Wrap(err, "could not execute rule") - } - - if !applicable { - return fmt.Errorf("no rule was applicable for this request: %v", input.MetaInput.Input) - } - - return nil -} - -// executeGeneratedFuzzingRequest executes a generated fuzzing request after building it using rules and payloads -func (request *Request) executeGeneratedFuzzingRequest(gr fuzz.GeneratedRequest, input *contextargs.Context, callback protocols.OutputEventCallback) bool { - hasInteractMatchers := interactsh.HasMatchers(request.CompiledOperators) - hasInteractMarkers := len(gr.InteractURLs) > 0 - if request.options.HostErrorsCache != nil && request.options.HostErrorsCache.Check(request.options.ProtocolType.String(), input) { - return false - } - request.options.RateLimitTake() - req := &generatedRequest{ - request: gr.Request, - dynamicValues: gr.DynamicValues, - interactshURLs: gr.InteractURLs, - original: request, - fuzzGeneratedRequest: gr, - } - var gotMatches bool - requestErr := request.executeRequest(input, req, gr.DynamicValues, hasInteractMatchers, func(event *output.InternalWrappedEvent) { - for _, result := range event.Results { - result.IsFuzzingResult = true - result.FuzzingMethod = gr.Request.Method - result.FuzzingParameter = gr.Parameter - result.FuzzingPosition = gr.Component.Name() - } - - setInteractshCallback := false - if hasInteractMarkers && hasInteractMatchers && request.options.Interactsh != nil { - requestData := &interactsh.RequestData{ - MakeResultFunc: request.MakeResultEvent, - Event: event, - Operators: request.CompiledOperators, - MatchFunc: request.Match, - ExtractFunc: request.Extract, - Parameter: gr.Parameter, - Request: gr.Request, - } - setInteractshCallback = true - request.options.Interactsh.RequestEvent(gr.InteractURLs, requestData) - gotMatches = request.options.Interactsh.AlreadyMatched(requestData) - } else { - callback(event) - } - // Add the extracts to the dynamic values if any. - if event.OperatorsResult != nil { - gotMatches = event.OperatorsResult.Matched - } - if request.options.FuzzParamsFrequency != nil && !setInteractshCallback { - if !gotMatches { - request.options.FuzzParamsFrequency.MarkParameter(gr.Parameter, gr.Request.String(), request.options.TemplateID) - } else { - request.options.FuzzParamsFrequency.UnmarkParameter(gr.Parameter, gr.Request.String(), request.options.TemplateID) - } - } - }, 0) - // If a variable is unresolved, skip all further requests - if errors.Is(requestErr, ErrMissingVars) { - return false - } - if requestErr != nil { - gologger.Verbose().Msgf("[%s] Error occurred in request: %s\n", request.options.TemplateID, requestErr) - } - if request.options.HostErrorsCache != nil { - request.options.HostErrorsCache.MarkFailedOrRemove(request.options.ProtocolType.String(), input, requestErr) - } - request.options.Progress.IncrementRequests() - - // If this was a match, and we want to stop at first match, skip all further requests. - shouldStopAtFirstMatch := request.options.Options.StopAtFirstMatch || request.StopAtFirstMatch - if shouldStopAtFirstMatch && gotMatches { - return false - } - return true -} - -// ShouldFuzzTarget checks if given target should be fuzzed or not using `filter` field in template -func (request *Request) ShouldFuzzTarget(input *contextargs.Context) bool { - if len(request.FuzzPreCondition) == 0 { - return true - } - status := []bool{} - for index, filter := range request.FuzzPreCondition { - dataMap := request.filterDataMap(input) - // dump if svd is enabled - if request.options.Options.ShowVarDump { - gologger.Debug().Msgf("Fuzz Filter Variables: \n%s\n", vardump.DumpVariables(dataMap)) - } - isMatch, _ := request.Match(dataMap, filter) - status = append(status, isMatch) - if request.options.Options.MatcherStatus { - gologger.Debug().Msgf("[%s] [%s] Filter => %s : %v", input.MetaInput.Target(), request.options.TemplateID, operators.GetMatcherName(filter, index), isMatch) - } - } - if len(status) == 0 { - return true - } - var matched bool - if request.fuzzPreConditionOperator == matchers.ANDCondition { - matched = operators.EvalBoolSlice(status, true) - } else { - matched = operators.EvalBoolSlice(status, false) - } - if request.options.Options.MatcherStatus { - gologger.Debug().Msgf("[%s] [%s] Final Filter Status => %v", input.MetaInput.Target(), request.options.TemplateID, matched) - } - return matched -} - -// input data map returns map[string]interface{} from input -func (request *Request) filterDataMap(input *contextargs.Context) map[string]interface{} { - m := make(map[string]interface{}) - parsed, err := input.MetaInput.URL() - if err != nil { - m["host"] = input.MetaInput.Input - return m - } - m = protocolutils.GenerateVariables(parsed, true, m) - for k, v := range m { - m[strings.ToLower(k)] = v - } - m["path"] = parsed.Path // override existing - m["query"] = parsed.RawQuery - // add request data like headers, body etc - if input.MetaInput.ReqResp != nil && input.MetaInput.ReqResp.Request != nil { - req := input.MetaInput.ReqResp.Request - m["method"] = req.Method - m["body"] = req.Body - - sb := &strings.Builder{} - req.Headers.Iterate(func(k, v string) bool { - k = strings.ToLower(strings.ReplaceAll(strings.TrimSpace(k), "-", "_")) - if strings.EqualFold(k, "Cookie") { - m["cookie"] = v - } - if strings.EqualFold(k, "User_Agent") { - m["user_agent"] = v - } - if strings.EqualFold(k, "content_type") { - m["content_type"] = v - } - _, _ = fmt.Fprintf(sb, "%s: %s\n", k, v) - return true - }) - m["header"] = sb.String() - } else { - // add default method value - m["method"] = http.MethodGet - } - return m -} +package http + +// === Fuzzing Documentation (Scoped to this File) ===== +// -> request.executeFuzzingRule [iterates over payloads(+requests) and executes] +// -> request.executePayloadUsingRules [executes single payload on all rules (if more than 1)] +// -> request.executeGeneratedFuzzingRequest [execute final generated fuzzing request and get result] + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/pkg/errors" + "github.com/projectdiscovery/gologger" + "github.com/projectdiscovery/nuclei/v3/pkg/fuzz" + "github.com/projectdiscovery/nuclei/v3/pkg/fuzz/analyzers" + "github.com/projectdiscovery/nuclei/v3/pkg/operators" + "github.com/projectdiscovery/nuclei/v3/pkg/operators/matchers" + "github.com/projectdiscovery/nuclei/v3/pkg/output" + "github.com/projectdiscovery/nuclei/v3/pkg/protocols" + "github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs" + "github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/generators" + "github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/interactsh" + "github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/utils/vardump" + protocolutils "github.com/projectdiscovery/nuclei/v3/pkg/protocols/utils" + "github.com/projectdiscovery/nuclei/v3/pkg/types" + "github.com/projectdiscovery/retryablehttp-go" + "github.com/projectdiscovery/useragent" + urlutil "github.com/projectdiscovery/utils/url" +) + +// executeFuzzingRule executes fuzzing request for a URL +// TODO: +// 1. use SPMHandler and rewrite stop at first match logic here +// 2. use scanContext instead of contextargs.Context +func (request *Request) executeFuzzingRule(input *contextargs.Context, previous output.InternalEvent, callback protocols.OutputEventCallback) error { + // methdology: + // to check applicablity of rule, we first try to execute it with one value + // if it is applicable, we execute all requests + // if it is not applicable, we log and fail silently + + // check if target should be fuzzed or not + if !request.ShouldFuzzTarget(input) { + urlx, _ := input.MetaInput.URL() + if urlx != nil { + gologger.Verbose().Msgf("[%s] fuzz: target(%s) not applicable for fuzzing\n", request.options.TemplateID, urlx.String()) + } else { + gologger.Verbose().Msgf("[%s] fuzz: target(%s) not applicable for fuzzing\n", request.options.TemplateID, input.MetaInput.Input) + } + return nil + } + + if input.MetaInput.Input == "" && input.MetaInput.ReqResp == nil { + return errors.New("empty input provided for fuzzing") + } + + // ==== fuzzing when full HTTP request is provided ===== + + if input.MetaInput.ReqResp != nil { + baseRequest, err := input.MetaInput.ReqResp.BuildRequest() + if err != nil { + return errors.Wrap(err, "fuzz: could not build request obtained from target file") + } + request.addHeadersToRequest(baseRequest) + input.MetaInput.Input = baseRequest.String() + // execute with one value first to checks its applicability + err = request.executeAllFuzzingRules(input, previous, baseRequest, callback) + if err != nil { + // in case of any error, return it + if fuzz.IsErrRuleNotApplicable(err) { + // log and fail silently + gologger.Verbose().Msgf("[%s] fuzz: %s\n", request.options.TemplateID, err) + return nil + } + if errors.Is(err, ErrMissingVars) { + return err + } + gologger.Verbose().Msgf("[%s] fuzz: payload request execution failed: %s\n", request.options.TemplateID, err) + } + return nil + } + + // ==== fuzzing when only URL is provided ===== + + // we need to use this url instead of input + inputx := input.Clone() + parsed, err := urlutil.ParseAbsoluteURL(input.MetaInput.Input, true) + if err != nil { + return errors.Wrap(err, "fuzz: could not parse input url") + } + baseRequest, err := retryablehttp.NewRequestFromURL(http.MethodGet, parsed, nil) + if err != nil { + return errors.Wrap(err, "fuzz: could not build request from url") + } + userAgent := useragent.PickRandom() + baseRequest.Header.Set("User-Agent", userAgent.Raw) + request.addHeadersToRequest(baseRequest) + + // execute with one value first to checks its applicability + err = request.executeAllFuzzingRules(inputx, previous, baseRequest, callback) + if err != nil { + // in case of any error, return it + if fuzz.IsErrRuleNotApplicable(err) { + // log and fail silently + gologger.Verbose().Msgf("[%s] fuzz: %s\n", request.options.TemplateID, err) + return nil + } + if errors.Is(err, ErrMissingVars) { + return err + } + gologger.Verbose().Msgf("[%s] fuzz: payload request execution failed: %s\n", request.options.TemplateID, err) + } + return nil +} + +func (request *Request) addHeadersToRequest(baseRequest *retryablehttp.Request) { + for k, v := range request.Headers { + baseRequest.Header.Set(k, v) + } +} + +// executeAllFuzzingRules executes all fuzzing rules defined in template for a given base request +func (request *Request) executeAllFuzzingRules(input *contextargs.Context, values map[string]interface{}, baseRequest *retryablehttp.Request, callback protocols.OutputEventCallback) error { + applicable := false + values = generators.MergeMaps(request.filterDataMap(input), values) + for _, rule := range request.Fuzzing { + select { + case <-input.Context().Done(): + return input.Context().Err() + default: + } + + input := &fuzz.ExecuteRuleInput{ + Input: input, + DisplayFuzzPoints: request.options.Options.DisplayFuzzPoints, + Callback: func(gr fuzz.GeneratedRequest) bool { + select { + case <-input.Context().Done(): + return false + default: + } + + // TODO: replace this after scanContext Refactor + return request.executeGeneratedFuzzingRequest(gr, input, callback) + }, + Values: values, + BaseRequest: baseRequest.Clone(context.TODO()), + } + if request.Analyzer != nil { + analyzer := analyzers.GetAnalyzer(request.Analyzer.Name) + input.ApplyPayloadInitialTransformation = analyzer.ApplyInitialTransformation + if request.Analyzer.Parameters == nil { + request.Analyzer.Parameters = make(map[string]interface{}) + } + input.AnalyzerParams = request.Analyzer.Parameters + } + err := rule.Execute(input) + if err == nil { + applicable = true + continue + } + if fuzz.IsErrRuleNotApplicable(err) { + gologger.Verbose().Msgf("[%s] fuzz: %s\n", request.options.TemplateID, err) + continue + } + if err == types.ErrNoMoreRequests { + return nil + } + return errors.Wrap(err, "could not execute rule") + } + + if !applicable { + return fmt.Errorf("no rule was applicable for this request: %v", input.MetaInput.Input) + } + + return nil +} + +// executeGeneratedFuzzingRequest executes a generated fuzzing request after building it using rules and payloads +func (request *Request) executeGeneratedFuzzingRequest(gr fuzz.GeneratedRequest, input *contextargs.Context, callback protocols.OutputEventCallback) bool { + hasInteractMatchers := interactsh.HasMatchers(request.CompiledOperators) + hasInteractMarkers := len(gr.InteractURLs) > 0 + if request.options.HostErrorsCache != nil && request.options.HostErrorsCache.Check(request.options.ProtocolType.String(), input) { + return false + } + request.options.RateLimitTake() + req := &generatedRequest{ + request: gr.Request, + dynamicValues: gr.DynamicValues, + interactshURLs: gr.InteractURLs, + original: request, + fuzzGeneratedRequest: gr, + } + var gotMatches bool + requestErr := request.executeRequest(input, req, gr.DynamicValues, hasInteractMatchers, func(event *output.InternalWrappedEvent) { + for _, result := range event.Results { + result.IsFuzzingResult = true + result.FuzzingMethod = gr.Request.Method + result.FuzzingParameter = gr.Parameter + result.FuzzingPosition = gr.Component.Name() + } + + setInteractshCallback := false + if hasInteractMarkers && hasInteractMatchers && request.options.Interactsh != nil { + requestData := &interactsh.RequestData{ + MakeResultFunc: request.MakeResultEvent, + Event: event, + Operators: request.CompiledOperators, + MatchFunc: request.Match, + ExtractFunc: request.Extract, + Parameter: gr.Parameter, + Request: gr.Request, + } + setInteractshCallback = true + request.options.Interactsh.RequestEvent(gr.InteractURLs, requestData) + gotMatches = request.options.Interactsh.AlreadyMatched(requestData) + } else { + callback(event) + } + // Add the extracts to the dynamic values if any. + if event.OperatorsResult != nil { + gotMatches = event.OperatorsResult.Matched + } + if request.options.FuzzParamsFrequency != nil && !setInteractshCallback { + if !gotMatches { + request.options.FuzzParamsFrequency.MarkParameter(gr.Parameter, gr.Request.String(), request.options.TemplateID) + } else { + request.options.FuzzParamsFrequency.UnmarkParameter(gr.Parameter, gr.Request.String(), request.options.TemplateID) + } + } + }, 0) + // If a variable is unresolved, skip all further requests + if errors.Is(requestErr, ErrMissingVars) { + return false + } + if requestErr != nil { + gologger.Verbose().Msgf("[%s] Error occurred in request: %s\n", request.options.TemplateID, requestErr) + } + if request.options.HostErrorsCache != nil { + request.options.HostErrorsCache.MarkFailedOrRemove(request.options.ProtocolType.String(), input, requestErr) + } + request.options.Progress.IncrementRequests() + + // If this was a match, and we want to stop at first match, skip all further requests. + shouldStopAtFirstMatch := request.options.Options.StopAtFirstMatch || request.StopAtFirstMatch + if shouldStopAtFirstMatch && gotMatches { + return false + } + return true +} + +// ShouldFuzzTarget checks if given target should be fuzzed or not using `filter` field in template +func (request *Request) ShouldFuzzTarget(input *contextargs.Context) bool { + if len(request.FuzzPreCondition) == 0 { + return true + } + status := []bool{} + for index, filter := range request.FuzzPreCondition { + dataMap := request.filterDataMap(input) + // dump if svd is enabled + if request.options.Options.ShowVarDump { + gologger.Debug().Msgf("Fuzz Filter Variables: \n%s\n", vardump.DumpVariables(dataMap)) + } + isMatch, _ := request.Match(dataMap, filter) + status = append(status, isMatch) + if request.options.Options.MatcherStatus { + gologger.Debug().Msgf("[%s] [%s] Filter => %s : %v", input.MetaInput.Target(), request.options.TemplateID, operators.GetMatcherName(filter, index), isMatch) + } + } + if len(status) == 0 { + return true + } + var matched bool + if request.fuzzPreConditionOperator == matchers.ANDCondition { + matched = operators.EvalBoolSlice(status, true) + } else { + matched = operators.EvalBoolSlice(status, false) + } + if request.options.Options.MatcherStatus { + gologger.Debug().Msgf("[%s] [%s] Final Filter Status => %v", input.MetaInput.Target(), request.options.TemplateID, matched) + } + return matched +} + +// input data map returns map[string]interface{} from input +func (request *Request) filterDataMap(input *contextargs.Context) map[string]interface{} { + m := make(map[string]interface{}) + parsed, err := input.MetaInput.URL() + if err != nil { + m["host"] = input.MetaInput.Input + return m + } + m = protocolutils.GenerateVariables(parsed, true, m) + for k, v := range m { + m[strings.ToLower(k)] = v + } + m["path"] = parsed.Path // override existing + m["query"] = parsed.RawQuery + // add request data like headers, body etc + if input.MetaInput.ReqResp != nil && input.MetaInput.ReqResp.Request != nil { + req := input.MetaInput.ReqResp.Request + m["method"] = req.Method + m["body"] = req.Body + + sb := &strings.Builder{} + req.Headers.Iterate(func(k, v string) bool { + k = strings.ToLower(strings.ReplaceAll(strings.TrimSpace(k), "-", "_")) + if strings.EqualFold(k, "Cookie") { + m["cookie"] = v + } + if strings.EqualFold(k, "User_Agent") { + m["user_agent"] = v + } + if strings.EqualFold(k, "content_type") { + m["content_type"] = v + } + _, _ = fmt.Fprintf(sb, "%s: %s\n", k, v) + return true + }) + m["header"] = sb.String() + } else { + // add default method value + m["method"] = http.MethodGet + } + return m +}