From 2b9c9858182d5f3ce2975857d45d89af9a3bb00c Mon Sep 17 00:00:00 2001 From: Dwi Siswanto <25837540+dwisiswant0@users.noreply.github.com> Date: Fri, 5 Dec 2025 21:55:41 +0700 Subject: [PATCH] fix(lib): segfault when init engine with `EnableHeadlessWithOpts` (#6602) * fix(lib): segfault when init engine with `EnableHeadlessWithOpts` The panic was caused by attempting to log a sandbox warning before the logger was initialized. RCA: * SDK option funcs were exec'd before logger init. * `EnableHeadlessWithOpts()` attempted to create browser instance & log warnings during the config phase. * `Logger` was only init'd later in `init()` phase. * This caused nil pointer dereference when `MustDisableSandbox()` returned true (root on Linux/Unix or Windows). Changes: * Init `Logger` in `types.DefaultOptions()` to ensure it's always available before any option functions execute. * Init `Logger` field in both `NewNucleiEngineCtx()` and `NewThreadSafeNucleiEngineCtx()` from `defaultOptions.Logger`. * Move browser instance creation from `EnableHeadlessWithOpts()` to the `init()` phase where `Logger` is guaranteed to be available. * Simplify logger sync logic in `init()` to only update if changed by `WithLogger` option. * Add test case to verify headless initialization works without panic. The fix maintains backward compatibility while make sure the logger is always available when needed by any SDK option function. Fixes #6601. Signed-off-by: Dwi Siswanto * build(make): adds `-timeout 30m -count 1` GOFLAGS in `test` cmd Signed-off-by: Dwi Siswanto * Revert "fix(lib): segfault when init engine with `EnableHeadlessWithOpts`" let see if this pass flaky test. This reverts commit 63fcb6a1cbe7a4db7a78be766affc70eb237e57e. * test(engine): let see if this pass flaky test Signed-off-by: Dwi Siswanto * Revert "Revert "fix(lib): segfault when init engine with `EnableHeadlessWithOpts`"" This reverts commit 62b4223803ccb1e93593e2e08e39923d76aa20b1. * test(engine): increase `TestActionNavigate` timeout Signed-off-by: Dwi Siswanto * Revert "test(engine): let see if this pass flaky test" This reverts commit d27cd985cff1b06aa1965ea11f8aa32f00778ab5. --------- Signed-off-by: Dwi Siswanto --- Makefile | 2 +- lib/config.go | 15 ++++--------- lib/multi.go | 8 ++++--- lib/sdk.go | 8 ++++--- lib/sdk_private.go | 21 +++++++++++++------ lib/sdk_test.go | 20 ++++++++++++++++++ .../headless/engine/page_actions_test.go | 2 +- pkg/types/types.go | 1 + 8 files changed, 52 insertions(+), 25 deletions(-) diff --git a/Makefile b/Makefile index d0d995bfc..719c3088c 100644 --- a/Makefile +++ b/Makefile @@ -75,7 +75,7 @@ syntax-docs: docgen syntax-docs: ./bin/docgen SYNTAX-REFERENCE.md nuclei-jsonschema.json -test: GOFLAGS = -race -v +test: GOFLAGS = -race -v -timeout 30m -count 1 test: $(GOTEST) $(GOFLAGS) ./... diff --git a/lib/config.go b/lib/config.go index f624ef42e..c9fc6e74a 100644 --- a/lib/config.go +++ b/lib/config.go @@ -19,7 +19,6 @@ import ( "github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/hosterrorscache" "github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/interactsh" "github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/utils/vardump" - "github.com/projectdiscovery/nuclei/v3/pkg/protocols/headless/engine" "github.com/projectdiscovery/nuclei/v3/pkg/templates/types" pkgtypes "github.com/projectdiscovery/nuclei/v3/pkg/types" ) @@ -196,8 +195,10 @@ type HeadlessOpts struct { } // EnableHeadless allows execution of headless templates -// *Use With Caution*: Enabling headless mode may open up attack surface due to browser usage -// and can be prone to exploitation by custom unverified templates if not properly configured +// +// Warning: enabling headless mode may open up attack surface due to browser +// usage and can be prone to exploitation by custom unverified templates if not +// properly configured. func EnableHeadlessWithOpts(hopts *HeadlessOpts) NucleiSDKOptions { return func(e *NucleiEngine) error { e.opts.Headless = true @@ -207,14 +208,6 @@ func EnableHeadlessWithOpts(hopts *HeadlessOpts) NucleiSDKOptions { e.opts.ShowBrowser = hopts.ShowBrowser e.opts.UseInstalledChrome = hopts.UseChrome } - if engine.MustDisableSandbox() { - e.Logger.Warning().Msgf("The current platform and privileged user will run the browser without sandbox") - } - browser, err := engine.New(e.opts) - if err != nil { - return err - } - e.browserInstance = browser return nil } } diff --git a/lib/multi.go b/lib/multi.go index ac0a78946..5b84a2e9e 100644 --- a/lib/multi.go +++ b/lib/multi.go @@ -87,10 +87,12 @@ type ThreadSafeNucleiEngine struct { func NewThreadSafeNucleiEngineCtx(ctx context.Context, opts ...NucleiSDKOptions) (*ThreadSafeNucleiEngine, error) { defaultOptions := types.DefaultOptions() defaultOptions.ExecutionId = xid.New().String() - // default options + e := &NucleiEngine{ - opts: defaultOptions, - mode: threadSafe, + opts: defaultOptions, + mode: threadSafe, + ctx: ctx, + Logger: defaultOptions.Logger, } for _, option := range opts { if err := option(e); err != nil { diff --git a/lib/sdk.go b/lib/sdk.go index 99523c79a..6bd2a93ba 100644 --- a/lib/sdk.go +++ b/lib/sdk.go @@ -320,10 +320,12 @@ func NewNucleiEngineCtx(ctx context.Context, options ...NucleiSDKOptions) (*Nucl // default options defaultOptions := types.DefaultOptions() defaultOptions.ExecutionId = xid.New().String() + e := &NucleiEngine{ - opts: defaultOptions, - mode: singleInstance, - ctx: ctx, + opts: defaultOptions, + mode: singleInstance, + ctx: ctx, + Logger: defaultOptions.Logger, } for _, option := range options { if err := option(e); err != nil { diff --git a/lib/sdk_private.go b/lib/sdk_private.go index ba394f024..aa8ad9fe3 100644 --- a/lib/sdk_private.go +++ b/lib/sdk_private.go @@ -13,7 +13,6 @@ import ( "github.com/logrusorgru/aurora" "github.com/pkg/errors" - "github.com/projectdiscovery/gologger" "github.com/projectdiscovery/gologger/levels" "github.com/projectdiscovery/httpx/common/httpx" "github.com/projectdiscovery/nuclei/v3/internal/runner" @@ -30,6 +29,7 @@ import ( "github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/interactsh" "github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolinit" "github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate" + "github.com/projectdiscovery/nuclei/v3/pkg/protocols/headless/engine" "github.com/projectdiscovery/nuclei/v3/pkg/protocols/http/httpclientpool" "github.com/projectdiscovery/nuclei/v3/pkg/templates" "github.com/projectdiscovery/nuclei/v3/pkg/testutils" @@ -97,13 +97,11 @@ func (e *NucleiEngine) applyRequiredDefaults(ctx context.Context) { // init func (e *NucleiEngine) init(ctx context.Context) error { - // Set a default logger if one isn't provided in the options - if e.opts.Logger != nil { + // Update logger ref (if it was changed by [WithLogger]) + // (Logger is already initialized) + if e.opts.Logger != e.Logger { e.Logger = e.opts.Logger - } else { - e.opts.Logger = &gologger.Logger{} } - e.Logger = e.opts.Logger if e.opts.Verbose { e.Logger.SetMaxLevel(levels.LevelVerbose) @@ -167,6 +165,17 @@ func (e *NucleiEngine) init(ctx context.Context) error { return err } + if e.opts.Headless { + if engine.MustDisableSandbox() { + e.Logger.Warning().Msgf("The current platform and privileged user will run the browser without sandbox") + } + browser, err := engine.New(e.opts) + if err != nil { + return err + } + e.browserInstance = browser + } + if e.catalog == nil { e.catalog = disk.NewCatalog(config.DefaultConfig.TemplatesDirectory) } diff --git a/lib/sdk_test.go b/lib/sdk_test.go index c86f8ebbf..85a241d02 100644 --- a/lib/sdk_test.go +++ b/lib/sdk_test.go @@ -35,3 +35,23 @@ func TestContextCancelNucleiEngine(t *testing.T) { } defer ne.Close() } + +func TestHeadlessOptionInitialization(t *testing.T) { + ne, err := nuclei.NewNucleiEngineCtx( + context.Background(), + nuclei.EnableHeadlessWithOpts(&nuclei.HeadlessOpts{ + PageTimeout: 20, + ShowBrowser: false, + UseChrome: false, + HeadlessOptions: []string{}, + }), + ) + + require.NoError(t, err, "could not create nuclei engine with headless options") + require.NotNil(t, ne, "nuclei engine should not be nil") + + // Verify logger is initialized + require.NotNil(t, ne.Logger, "logger should be initialized") + + defer ne.Close() +} diff --git a/pkg/protocols/headless/engine/page_actions_test.go b/pkg/protocols/headless/engine/page_actions_test.go index 88440a2d6..59ff75ef9 100644 --- a/pkg/protocols/headless/engine/page_actions_test.go +++ b/pkg/protocols/headless/engine/page_actions_test.go @@ -39,7 +39,7 @@ func TestActionNavigate(t *testing.T) { actions := []*Action{{ActionType: ActionTypeHolder{ActionType: ActionNavigate}, Data: map[string]string{"url": "{{BaseURL}}"}}, {ActionType: ActionTypeHolder{ActionType: ActionWaitLoad}}} - testHeadlessSimpleResponse(t, response, actions, 20*time.Second, func(page *Page, err error, out ActionData) { + testHeadlessSimpleResponse(t, response, actions, 60*time.Second, func(page *Page, err error, out ActionData) { require.Nilf(t, err, "could not run page actions") require.Equal(t, "Nuclei Test Page", page.Page().MustInfo().Title, "could not navigate correctly") }) diff --git a/pkg/types/types.go b/pkg/types/types.go index daed01908..0f6663f38 100644 --- a/pkg/types/types.go +++ b/pkg/types/types.go @@ -793,6 +793,7 @@ func DefaultOptions() *Options { MaxHostError: 30, ResponseReadSize: 10 * unitutils.Mega, ResponseSaveSize: unitutils.Mega, + Logger: &gologger.Logger{}, } }