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 <git@dw1.io>

* build(make): adds `-timeout 30m -count 1` GOFLAGS in `test` cmd

Signed-off-by: Dwi Siswanto <git@dw1.io>

* 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 <git@dw1.io>

* Revert "Revert "fix(lib): segfault when init engine with `EnableHeadlessWithOpts`""

This reverts commit 62b4223803ccb1e93593e2e08e39923d76aa20b1.

* test(engine): increase `TestActionNavigate` timeout

Signed-off-by: Dwi Siswanto <git@dw1.io>

* Revert "test(engine): let see if this pass flaky test"

This reverts commit d27cd985cff1b06aa1965ea11f8aa32f00778ab5.

---------

Signed-off-by: Dwi Siswanto <git@dw1.io>
This commit is contained in:
Dwi Siswanto
2025-12-05 21:55:41 +07:00
committed by GitHub
parent 2449d5628f
commit 2b9c985818
8 changed files with 52 additions and 25 deletions

View File

@@ -75,7 +75,7 @@ syntax-docs: docgen
syntax-docs: syntax-docs:
./bin/docgen SYNTAX-REFERENCE.md nuclei-jsonschema.json ./bin/docgen SYNTAX-REFERENCE.md nuclei-jsonschema.json
test: GOFLAGS = -race -v test: GOFLAGS = -race -v -timeout 30m -count 1
test: test:
$(GOTEST) $(GOFLAGS) ./... $(GOTEST) $(GOFLAGS) ./...

View File

@@ -19,7 +19,6 @@ import (
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/hosterrorscache" "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/interactsh"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/utils/vardump" "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" "github.com/projectdiscovery/nuclei/v3/pkg/templates/types"
pkgtypes "github.com/projectdiscovery/nuclei/v3/pkg/types" pkgtypes "github.com/projectdiscovery/nuclei/v3/pkg/types"
) )
@@ -196,8 +195,10 @@ type HeadlessOpts struct {
} }
// EnableHeadless allows execution of headless templates // 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 { func EnableHeadlessWithOpts(hopts *HeadlessOpts) NucleiSDKOptions {
return func(e *NucleiEngine) error { return func(e *NucleiEngine) error {
e.opts.Headless = true e.opts.Headless = true
@@ -207,14 +208,6 @@ func EnableHeadlessWithOpts(hopts *HeadlessOpts) NucleiSDKOptions {
e.opts.ShowBrowser = hopts.ShowBrowser e.opts.ShowBrowser = hopts.ShowBrowser
e.opts.UseInstalledChrome = hopts.UseChrome 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 return nil
} }
} }

View File

@@ -87,10 +87,12 @@ type ThreadSafeNucleiEngine struct {
func NewThreadSafeNucleiEngineCtx(ctx context.Context, opts ...NucleiSDKOptions) (*ThreadSafeNucleiEngine, error) { func NewThreadSafeNucleiEngineCtx(ctx context.Context, opts ...NucleiSDKOptions) (*ThreadSafeNucleiEngine, error) {
defaultOptions := types.DefaultOptions() defaultOptions := types.DefaultOptions()
defaultOptions.ExecutionId = xid.New().String() defaultOptions.ExecutionId = xid.New().String()
// default options
e := &NucleiEngine{ e := &NucleiEngine{
opts: defaultOptions, opts: defaultOptions,
mode: threadSafe, mode: threadSafe,
ctx: ctx,
Logger: defaultOptions.Logger,
} }
for _, option := range opts { for _, option := range opts {
if err := option(e); err != nil { if err := option(e); err != nil {

View File

@@ -320,10 +320,12 @@ func NewNucleiEngineCtx(ctx context.Context, options ...NucleiSDKOptions) (*Nucl
// default options // default options
defaultOptions := types.DefaultOptions() defaultOptions := types.DefaultOptions()
defaultOptions.ExecutionId = xid.New().String() defaultOptions.ExecutionId = xid.New().String()
e := &NucleiEngine{ e := &NucleiEngine{
opts: defaultOptions, opts: defaultOptions,
mode: singleInstance, mode: singleInstance,
ctx: ctx, ctx: ctx,
Logger: defaultOptions.Logger,
} }
for _, option := range options { for _, option := range options {
if err := option(e); err != nil { if err := option(e); err != nil {

View File

@@ -13,7 +13,6 @@ import (
"github.com/logrusorgru/aurora" "github.com/logrusorgru/aurora"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/projectdiscovery/gologger"
"github.com/projectdiscovery/gologger/levels" "github.com/projectdiscovery/gologger/levels"
"github.com/projectdiscovery/httpx/common/httpx" "github.com/projectdiscovery/httpx/common/httpx"
"github.com/projectdiscovery/nuclei/v3/internal/runner" "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/interactsh"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolinit" "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/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/protocols/http/httpclientpool"
"github.com/projectdiscovery/nuclei/v3/pkg/templates" "github.com/projectdiscovery/nuclei/v3/pkg/templates"
"github.com/projectdiscovery/nuclei/v3/pkg/testutils" "github.com/projectdiscovery/nuclei/v3/pkg/testutils"
@@ -97,13 +97,11 @@ func (e *NucleiEngine) applyRequiredDefaults(ctx context.Context) {
// init // init
func (e *NucleiEngine) init(ctx context.Context) error { func (e *NucleiEngine) init(ctx context.Context) error {
// Set a default logger if one isn't provided in the options // Update logger ref (if it was changed by [WithLogger])
if e.opts.Logger != nil { // (Logger is already initialized)
if e.opts.Logger != e.Logger {
e.Logger = e.opts.Logger e.Logger = e.opts.Logger
} else {
e.opts.Logger = &gologger.Logger{}
} }
e.Logger = e.opts.Logger
if e.opts.Verbose { if e.opts.Verbose {
e.Logger.SetMaxLevel(levels.LevelVerbose) e.Logger.SetMaxLevel(levels.LevelVerbose)
@@ -167,6 +165,17 @@ func (e *NucleiEngine) init(ctx context.Context) error {
return err 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 { if e.catalog == nil {
e.catalog = disk.NewCatalog(config.DefaultConfig.TemplatesDirectory) e.catalog = disk.NewCatalog(config.DefaultConfig.TemplatesDirectory)
} }

View File

@@ -35,3 +35,23 @@ func TestContextCancelNucleiEngine(t *testing.T) {
} }
defer ne.Close() 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()
}

View File

@@ -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}}} 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.Nilf(t, err, "could not run page actions")
require.Equal(t, "Nuclei Test Page", page.Page().MustInfo().Title, "could not navigate correctly") require.Equal(t, "Nuclei Test Page", page.Page().MustInfo().Title, "could not navigate correctly")
}) })

View File

@@ -793,6 +793,7 @@ func DefaultOptions() *Options {
MaxHostError: 30, MaxHostError: 30,
ResponseReadSize: 10 * unitutils.Mega, ResponseReadSize: 10 * unitutils.Mega,
ResponseSaveSize: unitutils.Mega, ResponseSaveSize: unitutils.Mega,
Logger: &gologger.Logger{},
} }
} }