Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cmd/args.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ type Args struct {
Header []string // Custom HTTP headers to add to requests
P []string // Parameters to test for XSS vulnerabilities
IgnoreParams []string // Parameters to ignore during scanning
OutOfScope []string // Domains to exclude from scanning

// String options
Config string // Path to configuration file
Expand Down Expand Up @@ -33,6 +34,7 @@ type Args struct {
ReportFormat string // Report format (plain, json, markdown, md)
HarFilePath string // Path to save HAR files
CustomBlindXSSPayloadFile string // Path to custom blind XSS payload file
OutOfScopeFile string // File containing domains to exclude

// Integer options
Timeout int // Request timeout in seconds
Expand Down
12 changes: 12 additions & 0 deletions cmd/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"time"

spinner "github.com/briandowns/spinner"
"github.com/hahwul/dalfox/v2/internal/optimization"
"github.com/hahwul/dalfox/v2/internal/printing"
model "github.com/hahwul/dalfox/v2/pkg/model"
"github.com/hahwul/dalfox/v2/pkg/scanning"
Expand Down Expand Up @@ -111,6 +112,10 @@ func runRawDataMode(filePath string, cmd *cobra.Command) {
target = "https://" + host + path
}
}
if optimization.IsOutOfScope(options, target) {
printing.DalLog("INFO", "Target is out of scope, skipping", options)
return
}
_, _ = scanning.Scan(target, options, "single")
}

Expand Down Expand Up @@ -144,6 +149,10 @@ func runHarMode(filePath string, _ *cobra.Command, sf bool) {
options.Data = entry.Request.PostData.Text
}
options.Method = entry.Request.Method
if optimization.IsOutOfScope(options, turl) {
updateSpinner(options, sf, i, len(harObject.Log.Entries))
continue
}
_, _ = scanning.Scan(turl, options, strconv.Itoa(i))
updateSpinner(options, sf, i, len(harObject.Log.Entries))
}
Expand All @@ -170,6 +179,9 @@ func runFileMode(filePath string, cmd *cobra.Command, sf bool) {
return
}
targets := voltUtils.UniqueStringSlice(ff)
if len(options.OutOfScope) > 0 {
targets = optimization.FilterOutOfScopeTargets(options, targets)
}
Comment on lines +182 to +184
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The optimization.FilterOutOfScopeTargets function already checks if options.OutOfScope is empty and returns early. This outer if condition is therefore redundant and can be removed to simplify the code.

targets = optimization.FilterOutOfScopeTargets(options, targets)

printing.DalLog("SYSTEM", "Loaded "+strconv.Itoa(len(targets))+" target URLs", options)
multi, _ := cmd.Flags().GetBool("multicast")
mass, _ := cmd.Flags().GetBool("mass")
Expand Down
8 changes: 8 additions & 0 deletions cmd/pipe.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"time"

"github.com/briandowns/spinner"
"github.com/hahwul/dalfox/v2/internal/optimization"
"github.com/hahwul/dalfox/v2/internal/printing"
"github.com/hahwul/dalfox/v2/internal/utils"
model "github.com/hahwul/dalfox/v2/pkg/model"
Expand Down Expand Up @@ -58,6 +59,9 @@ func runPipeCmd(cmd *cobra.Command, args []string) {
targets = append(targets, target)
}
targets = voltUtils.UniqueStringSlice(targets)
if len(options.OutOfScope) > 0 {
targets = optimization.FilterOutOfScopeTargets(options, targets)
}
Comment on lines +62 to +64
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The optimization.FilterOutOfScopeTargets function already checks if options.OutOfScope is empty and returns early. This outer if condition is therefore redundant and can be removed to simplify the code.

targets = optimization.FilterOutOfScopeTargets(options, targets)

printing.DalLog("SYSTEM", "Loaded "+strconv.Itoa(len(targets))+" target urls", options)

multi, _ := cmd.Flags().GetBool("multicast")
Expand Down Expand Up @@ -132,6 +136,10 @@ func runRawDataPipeMode(cmd *cobra.Command) {
target = "https://" + host + path
}
}
if optimization.IsOutOfScope(options, target) {
printing.DalLog("INFO", "Target is out of scope, skipping", options)
return
}
_, _ = scanning.Scan(target, options, "single")
}

Expand Down
21 changes: 20 additions & 1 deletion cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
"text/template"
"time"
Expand All @@ -16,6 +17,7 @@ import (
"github.com/hahwul/dalfox/v2/internal/har"
"github.com/hahwul/dalfox/v2/internal/printing"
"github.com/hahwul/dalfox/v2/pkg/model"
voltFile "github.com/hahwul/volt/file"
"github.com/logrusorgru/aurora"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
Expand Down Expand Up @@ -89,6 +91,8 @@ func init() {
rootCmd.PersistentFlags().StringSliceVarP(&args.Header, "header", "H", []string{}, "Add custom headers to the request. Example: -H 'Authorization: Bearer <token>'")
rootCmd.PersistentFlags().StringSliceVarP(&args.P, "param", "p", []string{}, "Specify parameters to test. Example: -p 'username' -p 'password'")
rootCmd.PersistentFlags().StringSliceVar(&args.IgnoreParams, "ignore-param", []string{}, "Ignore specific parameters during scanning. Example: --ignore-param 'api_token' --ignore-param 'csrf_token'")
rootCmd.PersistentFlags().StringSliceVar(&args.OutOfScope, "out-of-scope", []string{}, "Exclude domains from scanning. Supports wildcards. Example: --out-of-scope 'stg.example.com' --out-of-scope '*.dev.example.com'")
rootCmd.PersistentFlags().StringVar(&args.OutOfScopeFile, "out-of-scope-file", "", "Load out-of-scope domains from a file (one per line). Example: --out-of-scope-file 'exclusions.txt'")

// String
rootCmd.PersistentFlags().StringVar(&args.Config, "config", "", "Load configuration from a file. Example: --config 'config.json'")
Expand Down Expand Up @@ -248,7 +252,7 @@ func initializeFlagGroups() {
flagMap := map[string][]string{
"Input": {"config", "custom-payload", "custom-blind-xss-payload", "data", "grep", "remote-payloads", "remote-wordlists", "har-file-path"},
"Request": {"header", "cookie", "user-agent", "method", "cookie-from-raw"},
"Scanning": {"param", "ignore-param", "blind", "timeout", "delay", "worker", "skip-headless", "deep-domxss", "waf-evasion", "skip-discovery", "force-headless-verification", "use-bav", "skip-bav", "skip-mining-dom", "skip-mining-dict", "skip-mining-all", "skip-xss-scanning", "only-custom-payload", "skip-grepping", "detailed-analysis", "fast-scan", "magic-char-test", "context-aware"},
"Scanning": {"param", "ignore-param", "out-of-scope", "out-of-scope-file", "blind", "timeout", "delay", "worker", "skip-headless", "deep-domxss", "waf-evasion", "skip-discovery", "force-headless-verification", "use-bav", "skip-bav", "skip-mining-dom", "skip-mining-dict", "skip-mining-all", "skip-xss-scanning", "only-custom-payload", "skip-grepping", "detailed-analysis", "fast-scan", "magic-char-test", "context-aware"},
"Mining": {"mining-dict-word", "mining-dict", "mining-dom"},
"Output": {"output", "format", "only-poc", "report", "output-all", "output-request", "output-response", "poc-type", "report-format", "silence", "no-color", "no-spinner"},
"Advanced": {"custom-alert-value", "custom-alert-type", "found-action", "found-action-shell", "proxy", "ignore-return", "max-cpu", "only-discovery", "follow-redirects", "debug"},
Expand Down Expand Up @@ -317,6 +321,7 @@ func initConfig() {
Grep: args.Grep,
IgnoreReturn: args.IgnoreReturn,
IgnoreParams: args.IgnoreParams,
OutOfScope: args.OutOfScope,
Timeout: args.Timeout,
Concurrence: args.Concurrence,
MaxCPU: args.MaxCPU,
Expand Down Expand Up @@ -416,6 +421,9 @@ func initConfig() {
if len(args.IgnoreParams) == 0 && len(cfgOptions.IgnoreParams) > 0 {
options.IgnoreParams = cfgOptions.IgnoreParams
}
if len(args.OutOfScope) == 0 && len(cfgOptions.OutOfScope) > 0 {
options.OutOfScope = cfgOptions.OutOfScope
}
if args.Timeout == DefaultTimeout && cfgOptions.Timeout != 0 {
options.Timeout = cfgOptions.Timeout
}
Expand Down Expand Up @@ -456,6 +464,17 @@ func initConfig() {
}
}

// Load out-of-scope domains from file if specified
if args.OutOfScopeFile != "" {
domains, err := voltFile.ReadLinesOrLiteral(args.OutOfScopeFile)
if err != nil {
printing.DalLog("ERROR", "Failed to read out-of-scope file: "+err.Error(), options)
os.Exit(1)
}
options.OutOfScope = append(options.OutOfScope, domains...)
printing.DalLog("SYSTEM", "Loaded "+strconv.Itoa(len(domains))+" domains from out-of-scope file", options)
}

// If HarFilePath is specified via CLI or configuration file, initialize HAR writer
if options.HarFilePath != "" {
harFilePath = options.HarFilePath
Expand Down
5 changes: 5 additions & 0 deletions cmd/sxss.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cmd

import (
"github.com/hahwul/dalfox/v2/internal/optimization"
"github.com/hahwul/dalfox/v2/internal/printing"
"github.com/hahwul/dalfox/v2/pkg/scanning"
"github.com/spf13/cobra"
Expand Down Expand Up @@ -36,6 +37,10 @@ func runSxssCmd(cmd *cobra.Command, args []string) {

if options.Trigger != "" {
printing.DalLog("SYSTEM", "Using Stored XSS mode", options)
if optimization.IsOutOfScope(options, args[0]) {
printing.DalLog("INFO", "Target is out of scope, skipping", options)
return
}
if options.Format == "json" {
printing.DalLog("PRINT", "[", options)
}
Expand Down
5 changes: 5 additions & 0 deletions cmd/url.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cmd

import (
"github.com/hahwul/dalfox/v2/internal/optimization"
"github.com/hahwul/dalfox/v2/internal/printing"
"github.com/hahwul/dalfox/v2/pkg/scanning"
"github.com/spf13/cobra"
Expand All @@ -23,6 +24,10 @@ func runURLCmd(cmd *cobra.Command, args []string) {

printing.Summary(options, args[0])
printing.DalLog("SYSTEM", "Using single target mode", options)
if optimization.IsOutOfScope(options, args[0]) {
printing.DalLog("INFO", "Target is out of scope, skipping", options)
return
}
Comment on lines +27 to +30
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This out-of-scope check logic is duplicated across multiple command files (url.go, sxss.go, pipe.go, file.go). To improve maintainability and reduce code duplication, consider extracting this into a shared helper function within the cmd package. For example:

func isTargetOutOfScope(options model.Options, target string) bool {
	if optimization.IsOutOfScope(options, target) {
		printing.DalLog("INFO", "Target is out of scope, skipping", options)
		return true
	}
	return false
}

Then you could simplify this block to:

if isTargetOutOfScope(options, args[0]) {
    return
}

if options.Format == "json" {
printing.DalLog("PRINT", "[", options)
}
Expand Down
67 changes: 67 additions & 0 deletions internal/optimization/inspectionDomain.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package optimization

import (
"net/url"
"strings"

"github.com/hahwul/dalfox/v2/pkg/model"
)

// IsOutOfScope checks if a URL's host matches any out-of-scope pattern.
// Supports wildcard matching:
// - "stg.example.com" = exact match only
// - "*.stg.example.com" = matches subdomains (api.stg.example.com, devapi.stg.example.com)
func IsOutOfScope(options model.Options, targetURL string) bool {
if len(options.OutOfScope) == 0 {
return false
}

parsedURL, err := url.Parse(targetURL)
if err != nil {
// Treat malformed URLs as out-of-scope for safety
return true
}

host := strings.ToLower(parsedURL.Hostname())
// Handle URLs without scheme - url.Parse puts them in Path with empty Host
if host == "" && parsedURL.Path != "" {
parsedURL, err = url.Parse("http://" + targetURL)
if err != nil {
return true
}
host = strings.ToLower(parsedURL.Hostname())
}
for _, pattern := range options.OutOfScope {
pattern = strings.ToLower(strings.TrimSpace(pattern))
if matchDomainPattern(host, pattern) {
return true
}
}
return false
}

// matchDomainPattern checks if host matches the pattern.
// Pattern "*.example.com" matches "sub.example.com" but not "example.com"
// Pattern "example.com" matches only "example.com" exactly
func matchDomainPattern(host, pattern string) bool {
if strings.HasPrefix(pattern, "*.") {
suffix := pattern[1:] // ".example.com"
return strings.HasSuffix(host, suffix)
}
return host == pattern
}

// FilterOutOfScopeTargets removes out-of-scope URLs from a target list
func FilterOutOfScopeTargets(options model.Options, targets []string) []string {
if len(options.OutOfScope) == 0 {
return targets
}

filtered := make([]string, 0, len(targets))
for _, target := range targets {
if !IsOutOfScope(options, target) {
filtered = append(filtered, target)
}
}
return filtered
}
Loading
Loading