Compare commits

...
Author SHA1 Message Date
Mzack9999 12ff99c503 Browser agnostic 2026-10-26 15:26:00 +01:00
Mzack9999 e8f48fbe47 Wait upload 2026-10-26 13:04:45 +01:00
Mzack9999 ffd70c3a90 Share timeout 2026-10-26 13:01:32 +01:00
Mzack9999 d04a1fe7ff Merge dev 2026-10-26 12:20:39 +01:00
Mzack9999 490e9c3f34 bump utils 2026-10-26 11:38:12 +01:00
Mzack9999 92ef4fde14 Fix fallback 2026-10-26 00:28:10 +01:00
Mzack9999 595980256b Merge dev 2026-10-26 00:22:16 +01:00
Mzack9999 65589e2369 merge dev 2026-09-25 14:42:11 +02:00
Mzack9999 da4e351fc6 merge dev 2026-09-24 23:02:17 +02:00
Mzack9999 fbeb87e55c update utils 2026-09-23 21:58:13 +02:00
Mzack9999 d3ce7ce640 merge dev 2026-09-23 20:43:05 +02:00
Mzack9999 cb876981ac merge dev 2026-09-14 17:54:08 +02:00
Mzack9999 c00538364f merge dev 2026-09-08 15:10:06 +02:00
Mzack9999 cdfda0d4d1 wait upload 2026-09-08 15:03:04 +02:00
Mzack9999 e89cb6bf88 update utils 2026-09-06 13:20:29 +04:00
Mzack9999 a50606fa26 handle fallback 2026-09-06 13:16:35 +04:00
Mzack9999 cbd4fc13da merge dev 2026-09-06 13:16:35 +04:00
Mzack9999 6e64f6907a merge dev 2026-08-26 12:31:52 +04:00
Mzack9999 371f2fb385 add chromeshell 2026-08-15 21:26:35 +04:00
6 changed files with 171 additions and 47 deletions

No files matched your search

+1 -1
View File
@@ -102,7 +102,7 @@ require (
github.com/projectdiscovery/tlsx v1.4.3-0.20261020052910-acb80c2c7c75
github.com/projectdiscovery/uncover v1.2.1
github.com/projectdiscovery/useragent v0.0.109
github.com/projectdiscovery/utils v0.11.12-0.20261025172519-64b3c221cfe5
github.com/projectdiscovery/utils v0.11.12-0.20261026114542-c746c2a9b8ec
github.com/projectdiscovery/wappalyzergo v0.3.5
github.com/projectdiscovery/yamldoc-go v1.0.7
github.com/redis/go-redis/v9 v9.19.0
+2 -2
View File
@@ -999,8 +999,8 @@ github.com/projectdiscovery/uncover v1.2.1 h1:8U46T/96CLT7BPoXBgkTvWqB06lOyeTSLv
github.com/projectdiscovery/uncover v1.2.1/go.mod h1:0p8onrWxfpXQEYs90ZDzTSpu1107gWmodX1NWqu/+z4=
github.com/projectdiscovery/useragent v0.0.109 h1:b86BPdZvmgbJousa04jAugDRY4Y+rAvEamE0TFCPmeE=
github.com/projectdiscovery/useragent v0.0.109/go.mod h1:sRyBqDAcD31RKYrO8fyXGowLd36fNwY1g0MNW9mp7zk=
github.com/projectdiscovery/utils v0.11.12-0.20261025172519-64b3c221cfe5 h1:AOspf7ScbwZr8A2YnjCFd4ZJAMSFqzfEaIJIbJPuu10=
github.com/projectdiscovery/utils v0.11.12-0.20261025172519-64b3c221cfe5/go.mod h1:TJgsyVjB02CavwZVFzUzPg6YOSJXQxLhL808JCiQKlU=
github.com/projectdiscovery/utils v0.11.12-0.20261026114542-c746c2a9b8ec h1:yoAPKM61ofYFGaUUSqTCW4J0B4mHJoTg5FDv0ZrCDb4=
github.com/projectdiscovery/utils v0.11.12-0.20261026114542-c746c2a9b8ec/go.mod h1:TJgsyVjB02CavwZVFzUzPg6YOSJXQxLhL808JCiQKlU=
github.com/projectdiscovery/wappalyzergo v0.3.5 h1:UZradYF1GIcruSIV5wwo/Coph9/VnM17oajLD9eXR2U=
github.com/projectdiscovery/wappalyzergo v0.3.5/go.mod h1:4E6+fh/ckaY3wo5tlM7lNYSkhuTYRHzu2YmGIRraXP0=
github.com/projectdiscovery/yamldoc-go v1.0.7 h1:Odmjd1o4EkMff2F6QHi1dIYPIQPZUFy24YqmQCprmTk=
@@ -23,6 +23,11 @@ headless:
args:
by: x
xpath: /html/body/form/input[2]
# the upload response echoes the file, rendered as text in a pre element
- action: waitvisible
args:
by: xpath
xpath: //pre[contains(., "Basic File Upload")]
matchers:
- type: word
words:
+92 -42
View File
@@ -1,6 +1,7 @@
package engine
import (
"context"
"fmt"
"net/http"
"os"
@@ -12,7 +13,9 @@ import (
"github.com/go-rod/rod/lib/launcher/flags"
"github.com/pkg/errors"
"github.com/projectdiscovery/gologger"
"github.com/projectdiscovery/nuclei/v3/pkg/types"
"github.com/projectdiscovery/utils/chromeshell"
fileutil "github.com/projectdiscovery/utils/file"
osutils "github.com/projectdiscovery/utils/os"
)
@@ -38,7 +41,7 @@ func New(options *types.Options) (*Browser, error) {
var proxyServer *headlessProxy
var err error
chromeLauncher := launcher.New()
var chromeLauncher *launcher.Launcher
if options.CDPEndpoint == "" {
dataStore, err = os.MkdirTemp("", "nuclei-*")
@@ -46,58 +49,51 @@ func New(options *types.Options) (*Browser, error) {
return nil, errors.Wrap(err, "could not create temporary directory")
}
chromeLauncher = chromeLauncher.
Leakless(false).
Set("disable-crash-reporter").
Set("disable-gpu").
Set("disable-notifications").
Set("hide-scrollbars").
Set("ignore-certificate-errors").
Set("ignore-ssl-errors").
Set("incognito").
Set("mute-audio").
Set("window-size", fmt.Sprintf("%d,%d", 1080, 1920)).
Delete("use-mock-keychain").
UserDataDir(dataStore)
newChromeLauncher := func(browserPath string) *launcher.Launcher {
configured := launcher.New().
Leakless(false).
Set("disable-crash-reporter").
Set("disable-gpu").
Set("disable-notifications").
Set("hide-scrollbars").
Set("ignore-certificate-errors").
Set("ignore-ssl-errors").
Set("incognito").
Set("mute-audio").
Set("window-size", fmt.Sprintf("%d,%d", 1080, 1920)).
Delete("use-mock-keychain").
UserDataDir(dataStore)
if MustDisableSandbox() {
chromeLauncher = chromeLauncher.NoSandbox(true)
if browserPath != "" {
configured.Bin(browserPath)
}
if MustDisableSandbox() {
configured.NoSandbox(true)
}
configured.Headless(!options.ShowBrowser)
configured.
Proxy(proxyServer.Addr()).
Set("proxy-bypass-list", "<-loopback>")
for k, v := range options.ParseHeadlessOptionalArguments() {
configured.Set(flags.Flag(k), v)
}
return configured
}
executablePath, err := os.Executable()
browserPath, isChromeShell, err := browserPath(options)
if err != nil {
return nil, err
}
// if musl is used, most likely we are on alpine linux which is not supported by go-rod, so we fallback to default chrome
useMusl, _ := fileutil.UseMusl(executablePath)
if options.UseInstalledChrome || useMusl {
if chromePath, hasChrome := launcher.LookPath(); hasChrome {
chromeLauncher.Bin(chromePath)
} else {
return nil, errors.New("the chrome browser is not installed")
}
}
if options.ShowBrowser {
chromeLauncher = chromeLauncher.Headless(false)
} else {
chromeLauncher = chromeLauncher.Headless(true)
}
proxyServer, err = newHeadlessProxy(options)
if err != nil {
return nil, err
}
chromeLauncher = chromeLauncher.
Proxy(proxyServer.Addr()).
Set("proxy-bypass-list", "<-loopback>")
for k, v := range options.ParseHeadlessOptionalArguments() {
chromeLauncher.Set(flags.Flag(k), v)
}
launcherURL, err = chromeLauncher.Launch()
launcherURL, err = launchBrowser(browserPath, isChromeShell, func(path string) (string, error) {
chromeLauncher = newChromeLauncher(path)
return chromeLauncher.Launch()
})
if err != nil {
_ = proxyServer.Close()
return nil, err
}
} else {
@@ -143,6 +139,60 @@ func New(options *types.Options) (*Browser, error) {
return engine, nil
}
// browserPath returns the browser binary to launch, empty for the rod default,
// and whether that binary is chrome-headless-shell.
func browserPath(options *types.Options) (string, bool, error) {
executablePath, err := os.Executable()
if err != nil {
return "", false, err
}
// if musl is used, most likely we are on alpine linux which is not supported by go-rod, so we fallback to default chrome
useMusl, _ := fileutil.UseMusl(executablePath)
if options.UseInstalledChrome || useMusl {
if chromePath, hasChrome := launcher.LookPath(); hasChrome {
return chromePath, false, nil
}
return "", false, errors.New("the chrome browser is not installed")
}
if options.ShowBrowser || !chromeshell.Supported() {
return "", false, nil
}
// Prefer chrome-headless-shell for local headless templates; skip it when
// headed since the shell binary cannot show a UI.
ctx, cancel := context.WithTimeout(context.Background(), chromeshell.DefaultDownloadTimeout)
defer cancel()
shellPath, err := chromeshell.EnsureWithOptions(ctx, chromeshell.Options{Proxy: downloadProxy(options)})
if err != nil {
gologger.Warning().Msgf("Could not prepare chrome-headless-shell, using the default browser: %s\n", err)
return "", false, nil
}
return shellPath, true, nil
}
// downloadProxy returns the scan proxy for the chrome-headless-shell download,
// or empty to use the environment proxy.
func downloadProxy(options *types.Options) string {
if options.AliveHttpProxy != "" {
return options.AliveHttpProxy
}
return options.AliveSocksProxy
}
// launchBrowser launches browserPath and, only when it is chrome-headless-shell,
// retries once with the default browser. An installed Chrome that fails to
// launch is returned as an error, as before.
func launchBrowser(browserPath string, isChromeShell bool, launch func(string) (string, error)) (string, error) {
launcherURL, err := launch(browserPath)
if err == nil || !isChromeShell {
return launcherURL, err
}
gologger.Warning().Msgf("Could not launch chrome-headless-shell, using the default browser: %s\n", err)
return launch("")
}
// MustDisableSandbox determines if the current os and user needs sandbox mode disabled
func MustDisableSandbox() bool {
// linux with root user needs "--no-sandbox" option
@@ -0,0 +1,67 @@
package engine
import (
"errors"
"testing"
"github.com/projectdiscovery/nuclei/v3/pkg/types"
"github.com/stretchr/testify/require"
)
func TestLaunchBrowserFallsBackFromChromeShell(t *testing.T) {
var paths []string
got, err := launchBrowser("/cache/chrome-headless-shell", true, func(path string) (string, error) {
paths = append(paths, path)
if path != "" {
return "", errors.New("shell launch failed")
}
return "ws://default-browser", nil
})
require.NoError(t, err)
require.Equal(t, "ws://default-browser", got)
require.Equal(t, []string{"/cache/chrome-headless-shell", ""}, paths)
}
func TestLaunchBrowserDoesNotRetryDefaultBrowser(t *testing.T) {
var calls int
expected := errors.New("default launch failed")
_, err := launchBrowser("", false, func(string) (string, error) {
calls++
return "", expected
})
require.ErrorIs(t, err, expected)
require.Equal(t, 1, calls)
}
func TestLaunchBrowserDoesNotRetryInstalledChrome(t *testing.T) {
var paths []string
expected := errors.New("installed chrome launch failed")
_, err := launchBrowser("/usr/bin/google-chrome", false, func(path string) (string, error) {
paths = append(paths, path)
return "", expected
})
require.ErrorIs(t, err, expected)
require.Equal(t, []string{"/usr/bin/google-chrome"}, paths)
}
// The browser download goes through the scan proxy, or the environment proxy
// when there is none.
func TestDownloadProxy(t *testing.T) {
cases := []struct {
name string
opts *types.Options
want string
}{
{"no proxy uses the environment", &types.Options{}, ""},
{"http proxy", &types.Options{AliveHttpProxy: "http://127.0.0.1:8080"}, "http://127.0.0.1:8080"},
{"socks proxy", &types.Options{AliveSocksProxy: "socks5://127.0.0.1:1080"}, "socks5://127.0.0.1:1080"},
}
for _, c := range cases {
if got := downloadProxy(c.opts); got != c.want {
t.Errorf("%s: downloadProxy()=%q want %q", c.name, got, c.want)
}
}
}
+4 -2
View File
@@ -117,8 +117,10 @@ func TestRunReportsNavigationResponseWhenActionsFail(t *testing.T) {
var got *http.Response
actions := []*Action{{ActionType: ActionTypeHolder{ActionType: ActionNavigate}, Data: map[string]string{"url": "{{BaseURL}}"}}, {ActionType: ActionTypeHolder{ActionType: ActionWaitLoad}}}
_, _, err = instance.Run(input, actions, nil, &Options{Timeout: 20 * time.Second, Options: opts, OnNavigationResponse: func(resp *http.Response) { got = resp }})
require.Error(t, err, "the browser fails a navigation to an empty error page")
// Full Chromium fails a navigation to an empty error page, while
// chrome-headless-shell renders it, so only the reported response is
// checked: it must arrive whether or not the actions fail.
_, _, _ = instance.Run(input, actions, nil, &Options{Timeout: 20 * time.Second, Options: opts, OnNavigationResponse: func(resp *http.Response) { got = resp }})
require.NotNil(t, got)
require.Equal(t, http.StatusTooManyRequests, got.StatusCode)
require.Equal(t, "7", got.Header.Get("Retry-After"))