Dwi Siswanto a326f3925c
fix(tmplexec): memory blowup in multiproto (#6258)
* bugfix: fix memory blowup using previousEvent for multi-proto execution

* refactor(tmplexec): uses supported protocol types

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

* add co-author

Co-authored-by: Nakul Bharti <knakul853@users.noreply.github.com>
Signed-off-by: Dwi Siswanto <git@dw1.io>

* refactor(tmplexec): mv builder inside loop scope

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

* refactor(tmplexec): skip existing keys in `FillPreviousEvent`

The `FillPreviousEvent` func was modified to
prevent overwriting/duplicating entries in the
previous map.

It now checks if a key `k` from
`event.InternalEvent` already exists in the
previous map. If it does, the key is skipped. This
ensures that if `k` was already set (potentially
w/o a prefix), it's not re-added with an `ID_`
prefix.

Additionally, keys in `event.InternalEvent` that
already start with the current `ID_` prefix are
also skipped to avoid redundant prefixing.

This change simplifies the logic by removing the
`reqTypeWithIndexRegex` and directly addresses the
potential for duplicate / incorrectly prefixed
keys when `event.InternalEvent` grows during
protocol request execution.

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

* chore(tmplexec): naming convention, `ID` => `protoID`

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

* chore(tmplexec): it's request ID lol sorry

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

---------

Signed-off-by: Dwi Siswanto <git@dw1.io>
Co-authored-by: Ice3man <nizamulrana@gmail.com>
Co-authored-by: Nakul Bharti <knakul853@users.noreply.github.com>
2025-06-17 04:53:32 +05:30

96 lines
3.1 KiB
Go

package generic
import (
"sync/atomic"
"github.com/projectdiscovery/gologger"
"github.com/projectdiscovery/nuclei/v3/pkg/output"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols"
"github.com/projectdiscovery/nuclei/v3/pkg/scan"
"github.com/projectdiscovery/nuclei/v3/pkg/tmplexec/utils"
mapsutil "github.com/projectdiscovery/utils/maps"
)
// generic engine as name suggests is a generic template
// execution engine and executes all requests one after another
// without any logic in between
type Generic struct {
requests []protocols.Request
options *protocols.ExecutorOptions
results *atomic.Bool
}
// NewGenericEngine creates a new generic engine from a list of requests
func NewGenericEngine(requests []protocols.Request, options *protocols.ExecutorOptions, results *atomic.Bool) *Generic {
if results == nil {
results = &atomic.Bool{}
}
return &Generic{requests: requests, options: options, results: results}
}
// Compile engine specific compilation
func (g *Generic) Compile() error {
// protocol/ request is already handled by template executer
return nil
}
// ExecuteWithResults executes the template and returns results
func (g *Generic) ExecuteWithResults(ctx *scan.ScanContext) error {
dynamicValues := make(map[string]interface{})
if ctx.Input.HasArgs() {
ctx.Input.ForEach(func(key string, value interface{}) {
dynamicValues[key] = value
})
}
previous := mapsutil.NewSyncLockMap[string, any]()
for _, req := range g.requests {
select {
case <-ctx.Context().Done():
return ctx.Context().Err()
default:
}
inputItem := ctx.Input.Clone()
if g.options.InputHelper != nil && ctx.Input.MetaInput.Input != "" {
if inputItem.MetaInput.Input = g.options.InputHelper.Transform(inputItem.MetaInput.Input, req.Type()); inputItem.MetaInput.Input == "" {
return nil
}
}
err := req.ExecuteWithResults(inputItem, dynamicValues, output.InternalEvent(previous.GetAll()), func(event *output.InternalWrappedEvent) {
// this callback is not concurrent safe so mutex should be used to synchronize
if event == nil {
// ideally this should never happen since protocol exits on error and callback is not called
return
}
utils.FillPreviousEvent(req.GetID(), event, previous)
if event.HasOperatorResult() {
g.results.CompareAndSwap(false, true)
}
// for ExecuteWithResults : this callback will execute user defined callback and some error handling
// for Execute : this callback will print the result to output
ctx.LogEvent(event)
})
if err != nil {
ctx.LogError(err)
gologger.Warning().Msgf("[%s] Could not execute request for %s: %s\n", g.options.TemplateID, ctx.Input.MetaInput.PrettyPrint(), err)
}
if g.options.HostErrorsCache != nil {
g.options.HostErrorsCache.MarkFailedOrRemove(g.options.ProtocolType.String(), ctx.Input, err)
}
// If a match was found and stop at first match is set, break out of the loop and return
if g.results.Load() && (g.options.StopAtFirstMatch || g.options.Options.StopAtFirstMatch) {
break
}
}
return nil
}
// Type returns the type of engine
func (g *Generic) Name() string {
return "generic"
}