Repository: hahwul/dalfox

Issue:
--custom-blind-xss-payload myblindxsspayloads.txt
Hello,
Thank you very much for your great work.
Could you please add an option to choose our custom blind xss payloads from a file.
Because with this way we can use blind xss with encode and use multiple blind xss services.
And there is another idea that we can use a custom header from a file to inject the blind xss,
like this project https://github.com/adrianlarion/pegaxss
Thank you and best regards.
Emad.

Discussion:
@hahwul: 
Hi Emad,
Thanks for the great suggestions! I love the idea for custom blind XSS payloads. 
I'll work on adding it in the next update!@emadshanab: Thank you very much for your great work brother.
All the best.

Solution patch:
diff --git a/cmd/args.go b/cmd/args.go
index d309b82..9855fa6 100644
--- a/cmd/args.go
+++ b/cmd/args.go
@@ -28,6 +28,7 @@ type Args struct {
 	PoCType                   string
 	ReportFormat              string
 	HarFilePath               string
+	CustomBlindXSSPayloadFile string
 	Timeout                   int
 	Delay                     int
 	Concurrence               int
diff --git a/cmd/root.go b/cmd/root.go
index ca84fc1..cd543ee 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -69,6 +69,7 @@ func init() {
 	rootCmd.PersistentFlags().StringVarP(&args.Cookie, "cookie", "C", "", "Add custom cookies to the request. Example: -C 'sessionid=abc123'")
 	rootCmd.PersistentFlags().StringVarP(&args.Data, "data", "d", "", "Use POST method and add body data. Example: -d 'username=admin&password=admin'")
 	rootCmd.PersistentFlags().StringVar(&args.CustomPayload, "custom-payload", "", "Load custom payloads from a file. Example: --custom-payload 'payloads.txt'")
+	rootCmd.PersistentFlags().StringVar(&args.CustomBlindXSSPayloadFile, "custom-blind-xss-payload", "", "Load custom blind XSS payloads from a file. Example: --custom-blind-xss-payload 'payloads.txt'")
 	rootCmd.PersistentFlags().StringVar(&args.CustomAlertValue, "custom-alert-value", "1", "Set a custom alert value. Example: --custom-alert-value 'document.cookie'")
 	rootCmd.PersistentFlags().StringVar(&args.CustomAlertType, "custom-alert-type", "none", "Set a custom alert type. Example: --custom-alert-type 'str,none'")
 	rootCmd.PersistentFlags().StringVar(&args.UserAgent, "user-agent", "", "Set a custom User-Agent header. Example: --user-agent 'Mozilla/5.0'")
@@ -152,11 +153,12 @@ func initConfig() {
 	options = model.Options{
 		Header:            args.Header,
 		Cookie:            args.Cookie,
-		UniqParam:         args.P,
-		BlindURL:          args.Blind,
-		CustomPayloadFile: args.CustomPayload,
-		CustomAlertValue:  args.CustomAlertValue,
-		CustomAlertType:   args.CustomAlertType,
+		UniqParam:                 args.P,
+		BlindURL:                  args.Blind,
+		CustomPayloadFile:         args.CustomPayload,
+		CustomBlindXSSPayloadFile: args.CustomBlindXSSPayloadFile,
+		CustomAlertValue:          args.CustomAlertValue,
+		CustomAlertType:           args.CustomAlertType,
 		Data:              args.Data,
 		UserAgent:         args.UserAgent,
 		OutputFile:        args.Output,
@@ -225,6 +227,9 @@ func initConfig() {
 		if args.CustomPayload == "" && cfgOptions.CustomPayloadFile != "" {
 			options.CustomPayloadFile = cfgOptions.CustomPayloadFile
 		}
+		if args.CustomBlindXSSPayloadFile == "" && cfgOptions.CustomBlindXSSPayloadFile != "" {
+			options.CustomBlindXSSPayloadFile = cfgOptions.CustomBlindXSSPayloadFile
+		}
 		if args.CustomAlertValue == DefaultCustomAlertValue && cfgOptions.CustomAlertValue != "" {
 			options.CustomAlertValue = cfgOptions.CustomAlertValue
 		}
diff --git a/pkg/model/options.go b/pkg/model/options.go
index d504440..039453c 100644
--- a/pkg/model/options.go
+++ b/pkg/model/options.go
@@ -30,6 +30,7 @@ type Options struct {
 	// Feature Options
 	BlindURL                  string `json:"blind,omitempty"`
 	CustomPayloadFile         string `json:"custom-payload-file,omitempty"`
+	CustomBlindXSSPayloadFile string `json:"custom-blind-xss-payload-file,omitempty"`
 	CustomAlertValue          string `json:"custom-alert-value,omitempty"`
 	CustomAlertType           string `json:"custom-alert-type,omitempty"`
 	OnlyDiscovery             bool   `json:"only-discovery,omitempty"`
diff --git a/pkg/scanning/scan.go b/pkg/scanning/scan.go
index 067edeb..4feb134 100644
--- a/pkg/scanning/scan.go
+++ b/pkg/scanning/scan.go
@@ -161,6 +161,15 @@ func Scan(target string, options model.Options, sid string) (model.Result, error
 }
 
 // generatePayloads generates XSS payloads based on discovery results.
+// getBlindCallbackURL determines the correct format for the blind callback URL.
+// It assumes blindURL is not empty.
+func getBlindCallbackURL(blindURL string) string {
+	if strings.HasPrefix(blindURL, "https://") || strings.HasPrefix(blindURL, "http://") {
+		return blindURL
+	}
+	return "//" + blindURL
+}
+
 func generatePayloads(target string, options model.Options, policy map[string]string, pathReflection map[int]string, params map[string]model.ParamResult) (map[*http.Request]map[string]string, []string) {
 	query := make(map[*http.Request]map[string]string)
 	var durls []string
@@ -398,12 +407,7 @@ func generatePayloads(target string, options model.Options, policy map[string]st
 	// Blind Payload
 	if options.BlindURL != "" {
 		bpayloads := payload.GetBlindPayload()
-		var bcallback string
-		if strings.HasPrefix(options.BlindURL, "https://") || strings.HasPrefix(options.BlindURL, "http://") {
-			bcallback = options.BlindURL
-		} else {
-			bcallback = "//" + options.BlindURL
-		}
+		bcallback := getBlindCallbackURL(options.BlindURL)
 		for _, bpayload := range bpayloads {
 			bp := strings.Replace(bpayload, "CALLBACKURL", bcallback, 10)
 			tq, tm := optimization.MakeHeaderQuery(target, "Referer", bp, options)
@@ -432,6 +436,56 @@ func generatePayloads(target string, options model.Options, policy map[string]st
 		printing.DalLog("SYSTEM", "Added blind XSS payloads with callback URL: "+options.BlindURL, options)
 	}
 
+	// Custom Blind XSS Payloads from file
+	if options.CustomBlindXSSPayloadFile != "" {
+		fileInfo, statErr := os.Stat(options.CustomBlindXSSPayloadFile)
+		if os.IsNotExist(statErr) {
+			printing.DalLog("SYSTEM", "Failed to load custom blind XSS payload file: "+options.CustomBlindXSSPayloadFile+" (file not found)", options)
+		} else if statErr != nil {
+			printing.DalLog("SYSTEM", "Failed to load custom blind XSS payload file: "+options.CustomBlindXSSPayloadFile+" ("+statErr.Error()+")", options)
+		} else if fileInfo.IsDir() {
+			printing.DalLog("SYSTEM", "Failed to load custom blind XSS payload file: "+options.CustomBlindXSSPayloadFile+" (path is a directory)", options)
+		} else {
+			// File exists and is not a directory, proceed to read it
+			payloadLines, readErr := voltFile.ReadLinesOrLiteral(options.CustomBlindXSSPayloadFile)
+			if readErr != nil {
+				printing.DalLog("SYSTEM", "Failed to read custom blind XSS payload file: "+options.CustomBlindXSSPayloadFile+" ("+readErr.Error()+")", options)
+			} else {
+				var bcallback string
+				if options.BlindURL != "" {
+					bcallback = getBlindCallbackURL(options.BlindURL)
+				}
+
+				addedPayloadCount := 0
+				for _, customPayload := range payloadLines {
+					if customPayload != "" {
+						addedPayloadCount++
+						actualPayload := customPayload
+						if options.BlindURL != "" { // Only replace if BlindURL is set
+							actualPayload = strings.Replace(customPayload, "CALLBACKURL", bcallback, -1)
+						}
+
+						for k, v := range params {
+							if optimization.CheckInspectionParam(options, k) {
+								ptype := ""
+								for _, av := range v.Chars {
+									if strings.Contains(av, "PTYPE:") {
+										ptype = GetPType(av)
+									}
+								}
+								// Use only NaN encoder to avoid encoding issues with custom payloads
+								tq, tm := optimization.MakeRequestQuery(target, k, actualPayload, "toBlind"+ptype, "toBlind", NaN, options)
+								tm["payload"] = "toBlind"
+								query[tq] = tm
+							}
+						}
+					}
+				}
+				printing.DalLog("SYSTEM", "Added "+strconv.Itoa(addedPayloadCount)+" custom blind XSS payloads from file: "+options.CustomBlindXSSPayloadFile, options)
+			}
+		}
+	}
+
 	// Remote Payloads
 	if options.RemotePayloads != "" {
 		rp := strings.Split(options.RemotePayloads, ",")



Test patch:
diff --git a/pkg/scanning/scan_test.go b/pkg/scanning/scan_test.go
index 1e4133a..19986c3 100644
--- a/pkg/scanning/scan_test.go
+++ b/pkg/scanning/scan_test.go
@@ -2,13 +2,18 @@ package scanning
 
 import (
 	"fmt"
+	"io"
 	"net/http"
 	"net/http/httptest"
+	"os"
 	"strings"
+	"sync"
 	"testing"
 	"time"
 
 	"github.com/hahwul/dalfox/v2/pkg/model"
+	"github.com/logrusorgru/aurora"
+	"github.com/stretchr/testify/assert"
 )
 
 // mockServer creates a test server that reflects query parameters and path in its response
@@ -79,6 +84,198 @@ func Test_shouldIgnoreReturn(t *testing.T) {
 	}
 }
 
+// createTempPayloadFile creates a temporary file with the given content.
+// It returns the path to the temporary file and a cleanup function.
+func createTempPayloadFile(t *testing.T, content string) (string, func()) {
+	t.Helper()
+	tmpFile, err := os.CreateTemp("", "test-payloads-*.txt")
+	if err != nil {
+		t.Fatalf("Failed to create temp file: %v", err)
+	}
+	if _, err := tmpFile.WriteString(content); err != nil {
+		tmpFile.Close()
+		os.Remove(tmpFile.Name())
+		t.Fatalf("Failed to write to temp file: %v", err)
+	}
+	if err := tmpFile.Close(); err != nil {
+		os.Remove(tmpFile.Name())
+		t.Fatalf("Failed to close temp file: %v", err)
+	}
+	return tmpFile.Name(), func() { os.Remove(tmpFile.Name()) }
+}
+
+// captureOutput captures stdout and stderr during the execution of a function.
+func captureOutput(f func()) (string, string) {
+	oldStdout := os.Stdout
+	oldStderr := os.Stderr
+	rOut, wOut, _ := os.Pipe()
+	rErr, wErr, _ := os.Pipe()
+	os.Stdout = wOut
+	os.Stderr = wErr
+
+	f()
+
+	wOut.Close()
+	wErr.Close()
+	os.Stdout = oldStdout
+	os.Stderr = oldStderr
+
+	var outBuf, errBuf strings.Builder
+	// Use a WaitGroup to wait for copying to finish
+	var wg sync.WaitGroup
+	wg.Add(2)
+
+	go func() {
+		defer wg.Done()
+		io.Copy(&outBuf, rOut)
+	}()
+	go func() {
+		defer wg.Done()
+		io.Copy(&errBuf, rErr)
+	}()
+
+	wg.Wait()
+	return outBuf.String(), errBuf.String()
+}
+
+func TestGeneratePayloads_CustomBlindXSS(t *testing.T) {
+	server := mockServerForScanTest()
+	defer server.Close()
+
+	baseOptions := model.Options{
+		Concurrence:     1,
+		Format:          "plain",
+		Silence:         false, // Set to false to capture logs
+		NoSpinner:       true,
+		CustomAlertType: "none",
+		AuroraObject:    aurora.NewAurora(false), // Assuming NoColor is true for tests
+		Scan:            make(map[string]model.Scan),
+		PathReflection:  make(map[int]string),
+		Mutex:           &sync.Mutex{},
+	}
+
+	params := map[string]model.ParamResult{
+		"q": {
+			Name:      "q",
+			Type:      "URL",
+			Reflected: true,
+			Chars:     []string{},
+		},
+	}
+	policy := map[string]string{"Content-Type": "text/html"}
+	pathReflection := make(map[int]string)
+
+	t.Run("Valid custom blind payload file with --blind URL", func(t *testing.T) {
+		payloadContent := "blindy1<script>CALLBACKURL</script>\nblindy2<img src=x onerror=CALLBACKURL>"
+		payloadFile, cleanup := createTempPayloadFile(t, payloadContent)
+		defer cleanup()
+
+		options := baseOptions
+		options.CustomBlindXSSPayloadFile = payloadFile
+		options.BlindURL = "test-callback.com"
+		options.UniqParam = []string{"q"} // Ensure params are processed
+
+		var generatedQueries map[*http.Request]map[string]string
+		var logOutput string
+
+		stdout, stderr := captureOutput(func() {
+			generatedQueries, _ = generatePayloads(server.URL+"/?q=test", options, policy, pathReflection, params)
+		})
+		logOutput = stdout + stderr // Combine stdout and stderr
+
+		assert.Contains(t, logOutput, "Added 2 custom blind XSS payloads from file: "+payloadFile)
+
+		foundPayload1 := false
+		foundPayload2 := false
+		expectedPayload1 := strings.Replace("blindy1<script>CALLBACKURL</script>", "CALLBACKURL", "//"+options.BlindURL, -1)
+		expectedPayload2 := strings.Replace("blindy2<img src=x onerror=CALLBACKURL>", "CALLBACKURL", "//"+options.BlindURL, -1)
+
+		for req, meta := range generatedQueries {
+			if meta["type"] == "toBlind" && meta["payload"] == "toBlind" { // Check our specific type for these payloads
+				// Check if the payload in the query matches one of our expected transformed payloads
+				// This requires knowing how MakeRequestQuery structures the request.
+				// Assuming payload is in query parameter 'q' for this test.
+				queryValues := req.URL.Query()
+				if queryValues.Get("q") == expectedPayload1 {
+					foundPayload1 = true
+				}
+				if queryValues.Get("q") == expectedPayload2 {
+					foundPayload2 = true
+				}
+			}
+		}
+		assert.True(t, foundPayload1, "Expected payload 1 not found or not correctly transformed")
+		assert.True(t, foundPayload2, "Expected payload 2 not found or not correctly transformed")
+	})
+
+	t.Run("Custom blind payload file with CALLBACKURL but no --blind flag", func(t *testing.T) {
+		payloadContent := "blindy3<a href=CALLBACKURL>"
+		payloadFile, cleanup := createTempPayloadFile(t, payloadContent)
+		defer cleanup()
+
+		options := baseOptions
+		options.CustomBlindXSSPayloadFile = payloadFile
+		options.BlindURL = "" // No blind URL
+		options.UniqParam = []string{"q"}
+
+		var generatedQueries map[*http.Request]map[string]string
+		stdout, stderr := captureOutput(func() {
+			generatedQueries, _ = generatePayloads(server.URL+"/?q=test", options, policy, pathReflection, params)
+		})
+		logOutput := stdout + stderr // Combine stdout and stderr
+
+		assert.Contains(t, logOutput, "Added 1 custom blind XSS payloads from file: "+payloadFile)
+		foundPayload := false
+		expectedPayload := "blindy3<a href=CALLBACKURL>" // CALLBACKURL should not be replaced
+
+		for req, meta := range generatedQueries {
+			if meta["type"] == "toBlind" && meta["payload"] == "toBlind" {
+				if req.URL.Query().Get("q") == expectedPayload {
+					foundPayload = true
+					break
+				}
+			}
+		}
+		assert.True(t, foundPayload, "Expected payload with unreplaced CALLBACKURL not found")
+	})
+
+	t.Run("Invalid non-existent custom blind payload file", func(t *testing.T) {
+		options := baseOptions
+		options.CustomBlindXSSPayloadFile = "nonexistentfile.txt"
+		options.UniqParam = []string{"q"}
+
+		stdout, stderr := captureOutput(func() {
+			_, _ = generatePayloads(server.URL+"/?q=test", options, policy, pathReflection, params)
+		})
+		logOutput := stdout + stderr // Combine stdout and stderr
+
+		assert.Contains(t, logOutput, "Failed to load custom blind XSS payload file: nonexistentfile.txt")
+		// Check that no payloads of type "toBlind" were added due to this specific file error
+		// (assuming other payload generation might still occur)
+		customBlindPayloadsFound := false
+		assert.False(t, customBlindPayloadsFound, "Queries should not include payloads from a non-existent file if logic prevents it after error")
+	})
+
+	t.Run("Empty custom blind payload file", func(t *testing.T) {
+		payloadFile, cleanup := createTempPayloadFile(t, "")
+		defer cleanup()
+
+		options := baseOptions
+		options.CustomBlindXSSPayloadFile = payloadFile
+		options.UniqParam = []string{"q"}
+
+		stdout, stderr := captureOutput(func() {
+			_, _ = generatePayloads(server.URL+"/?q=test", options, policy, pathReflection, params)
+		})
+		logOutput := stdout + stderr // Combine stdout and stderr
+
+		assert.Contains(t, logOutput, "Added 0 custom blind XSS payloads from file: "+payloadFile)
+		// Verify no queries were generated specifically from this empty file.
+		// Similar to the above, this assumes no other "toBlind" payloads would be generated,
+		// or relies on the specific log message for confirmation.
+	})
+}
+
 func Test_generatePayloads(t *testing.T) {
 	// Create a mock server
 	server := mockServerForScanTest()