`,
+ }
+ }
+
+ 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(``, 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
+}