diff --git a/README.md b/README.md index 4929cb7cac..e60b5e2022 100644 --- a/README.md +++ b/README.md @@ -246,11 +246,12 @@ OPTIMIZATIONS: -no-stdin disable stdin processing HEADLESS: - -headless enable templates that require headless browser support (root user on Linux will disable sandbox) - -page-timeout int seconds to wait for each page in headless mode (default 20) - -sb, -show-browser show the browser on the screen when running templates with headless mode - -sc, -system-chrome use local installed Chrome browser instead of nuclei installed - -lha, -list-headless-action list available headless actions + -headless enable templates that require headless browser support (root user on Linux will disable sandbox) + -page-timeout int seconds to wait for each page in headless mode (default 20) + -sb, -show-browser show the browser on the screen when running templates with headless mode + -ho, -headless-options string[] start headless chrome with additional options + -sc, -system-chrome use local installed Chrome browser instead of nuclei installed + -lha, -list-headless-action list available headless actions DEBUG: -debug show all requests and responses diff --git a/v2/cmd/nuclei/main.go b/v2/cmd/nuclei/main.go index bd68cfc306..055555c808 100644 --- a/v2/cmd/nuclei/main.go +++ b/v2/cmd/nuclei/main.go @@ -272,6 +272,7 @@ on extensive configurability, massive extensibility and ease of use.`) flagSet.BoolVar(&options.Headless, "headless", false, "enable templates that require headless browser support (root user on Linux will disable sandbox)"), flagSet.IntVar(&options.PageTimeout, "page-timeout", 20, "seconds to wait for each page in headless mode"), flagSet.BoolVarP(&options.ShowBrowser, "show-browser", "sb", false, "show the browser on the screen when running templates with headless mode"), + flagSet.StringSliceVarP(&options.HeadlessOptionalArguments, "headless-options", "ho", nil, "start headless chrome with additional options", goflags.FileCommaSeparatedStringSliceOptions), flagSet.BoolVarP(&options.UseInstalledChrome, "system-chrome", "sc", false, "use local installed Chrome browser instead of nuclei installed"), flagSet.BoolVarP(&options.ShowActions, "list-headless-action", "lha", false, "list available headless actions"), ) diff --git a/v2/internal/runner/options.go b/v2/internal/runner/options.go index 3da37f45c4..ae3325055f 100644 --- a/v2/internal/runner/options.go +++ b/v2/internal/runner/options.go @@ -114,6 +114,10 @@ func validateOptions(options *types.Options) error { return errors.New("both verbose and silent mode specified") } + if (options.HeadlessOptionalArguments != nil || options.ShowBrowser || options.UseInstalledChrome) && !options.Headless { + return errors.New("headless mode (-headless) is required if -ho, -sb, -sc or -lha are set") + } + if options.FollowHostRedirects && options.FollowRedirects { return errors.New("both follow host redirects and follow redirects specified") } diff --git a/v2/internal/runner/options_test.go b/v2/internal/runner/options_test.go new file mode 100644 index 0000000000..72a9adb38b --- /dev/null +++ b/v2/internal/runner/options_test.go @@ -0,0 +1,61 @@ +package runner + +import ( + "strings" + "testing" + + "github.com/projectdiscovery/goflags" + "github.com/projectdiscovery/nuclei/v2/pkg/types" + "github.com/stretchr/testify/require" +) + +func TestParseHeadlessOptionalArguments(t *testing.T) { + tests := []struct { + name string + input string + want map[string]string + }{ + { + name: "single value", + input: "a=b", + want: map[string]string{"a": "b"}, + }, + { + name: "empty string", + input: "", + want: map[string]string{}, + }, + { + name: "empty key", + input: "=b", + want: map[string]string{}, + }, + { + name: "empty value", + input: "a=", + want: map[string]string{}, + }, + { + name: "double input", + input: "a=b,c=d", + want: map[string]string{"a": "b", "c": "d"}, + }, + { + name: "duplicated input", + input: "a=b,a=b", + want: map[string]string{"a": "b"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + strsl := goflags.StringSlice{} + for _, v := range strings.Split(tt.input, ",") { + //nolint + strsl.Set(v) + } + opt := types.Options{HeadlessOptionalArguments: strsl} + got := opt.ParseHeadlessOptionalArguments() + require.Equal(t, tt.want, got) + }) + } +} diff --git a/v2/pkg/protocols/headless/engine/engine.go b/v2/pkg/protocols/headless/engine/engine.go index 85e7af7b03..de43211d51 100644 --- a/v2/pkg/protocols/headless/engine/engine.go +++ b/v2/pkg/protocols/headless/engine/engine.go @@ -8,6 +8,7 @@ import ( "github.com/go-rod/rod" "github.com/go-rod/rod/lib/launcher" + "github.com/go-rod/rod/lib/launcher/flags" "github.com/pkg/errors" "github.com/projectdiscovery/nuclei/v2/pkg/types" @@ -75,6 +76,11 @@ func New(options *types.Options) (*Browser, error) { if types.ProxyURL != "" { chromeLauncher = chromeLauncher.Proxy(types.ProxyURL) } + + for k, v := range options.ParseHeadlessOptionalArguments() { + chromeLauncher.Set(flags.Flag(k), v) + } + launcherURL, err := chromeLauncher.Launch() if err != nil { return nil, err diff --git a/v2/pkg/types/types.go b/v2/pkg/types/types.go index 10411ea3f0..08922f3b8e 100644 --- a/v2/pkg/types/types.go +++ b/v2/pkg/types/types.go @@ -2,6 +2,7 @@ package types import ( "io" + "strings" "time" "github.com/projectdiscovery/goflags" @@ -195,6 +196,8 @@ type Options struct { Headless bool // ShowBrowser specifies whether the show the browser in headless mode ShowBrowser bool + // HeadlessOptionalArguments specifies optional arguments to pass to Chrome + HeadlessOptionalArguments goflags.StringSlice // NoTables disables pretty printing of cloud results in tables NoTables bool // DisableClustering disables clustering of templates @@ -439,3 +442,17 @@ func (options *Options) HasCloudOptions() bool { func (options *Options) ShouldUseHostError() bool { return options.MaxHostError > 0 && !options.NoHostErrors } + +func (options *Options) ParseHeadlessOptionalArguments() map[string]string { + optionalArguments := make(map[string]string) + for _, v := range options.HeadlessOptionalArguments { + if argParts := strings.SplitN(v, "=", 2); len(argParts) >= 2 { + key := strings.TrimSpace(argParts[0]) + value := strings.TrimSpace(argParts[1]) + if key != "" && value != "" { + optionalArguments[key] = value + } + } + } + return optionalArguments +}