Files
Dwi Siswanto 805bcfe76f fix(workflow): propagate context to child & race steps
Workflow inputs get updated with extracted values,
but later executors don't refresh their template
contexts from that state. Child subtemplates reuse
the same input object, letting sibling state leak
across concurrent executions. HTTP race requests
also ignore dynamic values when building requests.

Fix this by syncing the current workflow input
into each executor's template context, cloning
inputs passed to subtemplates, and forwarding
dynamic values to race request generation.

Fixes #5399

Signed-off-by: Dwi Siswanto <git@dw1.io>
2026-05-08 02:59:49 +07:00

191 lines
6.1 KiB
Go

package core
import (
"fmt"
"net/http/cookiejar"
"sync/atomic"
"github.com/projectdiscovery/gologger"
"github.com/projectdiscovery/nuclei/v3/pkg/output"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs"
"github.com/projectdiscovery/nuclei/v3/pkg/scan"
"github.com/projectdiscovery/nuclei/v3/pkg/workflows"
syncutil "github.com/projectdiscovery/utils/sync"
)
const workflowStepExecutionError = "[%s] Could not execute workflow step: %s\n"
// executeWorkflow runs a workflow on an input and returns true or false
func (e *Engine) executeWorkflow(ctx *scan.ScanContext, w *workflows.Workflow) bool {
results := &atomic.Bool{}
// at this point we should be at the start root execution of a workflow tree, hence we create global shared instances
workflowCookieJar, _ := cookiejar.New(nil)
ctxArgs := contextargs.New(ctx.Context())
ctxArgs.MetaInput = ctx.Input.MetaInput
ctxArgs.CookieJar = workflowCookieJar
// we can know the nesting level only at runtime, so the best we can do here is increase template threads by one unit in case it's equal to 1 to allow
// at least one subtemplate to go through, which it's idempotent to one in-flight template as the parent one is in an idle state
templateThreads := w.Options.Options.TemplateThreads
if templateThreads == 1 {
templateThreads++
}
swg, _ := syncutil.New(syncutil.WithSize(templateThreads))
for _, template := range w.Workflows {
newCtx := scan.NewScanContext(ctx.Context(), ctx.Input.Clone())
if err := e.runWorkflowStep(template, newCtx, results, swg, w); err != nil {
gologger.Warning().Msgf(workflowStepExecutionError, template.Template, err)
}
}
swg.Wait()
return results.Load()
}
// runWorkflowStep runs a workflow step for the workflow. It executes the workflow
// in a recursive manner running all subtemplates and matchers.
func (e *Engine) runWorkflowStep(template *workflows.WorkflowTemplate, ctx *scan.ScanContext, results *atomic.Bool, swg *syncutil.AdaptiveWaitGroup, w *workflows.Workflow) error {
var firstMatched bool
var err error
var mainErr error
if len(template.Matchers) == 0 {
for _, executer := range template.Executers {
e.syncWorkflowInputToTemplateCtx(executer, ctx.Input)
executer.Options.Progress.AddToTotal(int64(executer.Executer.Requests()))
// Don't print results with subtemplates, only print results on template.
if len(template.Subtemplates) > 0 {
ctx.OnResult = func(result *output.InternalWrappedEvent) {
if result.OperatorsResult == nil {
return
}
if len(result.Results) > 0 {
firstMatched = true
}
if result.OperatorsResult != nil && result.OperatorsResult.Extracts != nil {
for k, v := range result.OperatorsResult.Extracts {
// normalize items:
switch len(v) {
case 0, 1:
// - key:[item] => key: item
ctx.Input.Set(k, v[0])
default:
// - key:[item_0, ..., item_n] => key0:item_0, keyn:item_n
for vIdx, vVal := range v {
normalizedKIdx := fmt.Sprintf("%s%d", k, vIdx)
ctx.Input.Set(normalizedKIdx, vVal)
}
// also add the original name with full slice
ctx.Input.Set(k, v)
}
}
}
}
_, err = executer.Executer.ExecuteWithResults(ctx)
} else {
var matched bool
matched, err = executer.Executer.Execute(ctx)
if matched {
firstMatched = true
}
}
if w.Options.HostErrorsCache != nil {
w.Options.HostErrorsCache.MarkFailedOrRemove(w.Options.ProtocolType.String(), ctx.Input, err)
}
if err != nil {
if len(template.Executers) == 1 {
mainErr = err
} else {
gologger.Warning().Msgf(workflowStepExecutionError, template.Template, err)
}
continue
}
}
}
if len(template.Subtemplates) == 0 {
results.CompareAndSwap(false, firstMatched)
}
if len(template.Matchers) > 0 {
for _, executer := range template.Executers {
e.syncWorkflowInputToTemplateCtx(executer, ctx.Input)
executer.Options.Progress.AddToTotal(int64(executer.Executer.Requests()))
ctx.OnResult = func(event *output.InternalWrappedEvent) {
if event.OperatorsResult == nil {
return
}
if event.OperatorsResult.Extracts != nil {
for k, v := range event.OperatorsResult.Extracts {
ctx.Input.Set(k, v)
}
}
for _, matcher := range template.Matchers {
if !matcher.Match(event.OperatorsResult) {
continue
}
for _, subtemplate := range matcher.Subtemplates {
swg.Add()
go func(subtemplate *workflows.WorkflowTemplate) {
defer swg.Done()
// create a new context with the same input but with unset callbacks
// clone the Input so that other parallel executions won't overwrite the shared variables when subsequent templates are running
subCtx := scan.NewScanContext(ctx.Context(), ctx.Input.Clone())
if err := e.runWorkflowStep(subtemplate, subCtx, results, swg, w); err != nil {
gologger.Warning().Msgf(workflowStepExecutionError, subtemplate.Template, err)
}
}(subtemplate)
}
}
}
_, err := executer.Executer.ExecuteWithResults(ctx)
if err != nil {
if len(template.Executers) == 1 {
mainErr = err
} else {
gologger.Warning().Msgf(workflowStepExecutionError, template.Template, err)
}
continue
}
}
return mainErr
}
if len(template.Subtemplates) > 0 && firstMatched {
for _, subtemplate := range template.Subtemplates {
swg.Add()
go func(template *workflows.WorkflowTemplate) {
// create a new context with the same input but with unset callbacks
subCtx := scan.NewScanContext(ctx.Context(), ctx.Input.Clone())
if err := e.runWorkflowStep(template, subCtx, results, swg, w); err != nil {
gologger.Warning().Msgf(workflowStepExecutionError, template.Template, err)
}
swg.Done()
}(subtemplate)
}
}
return mainErr
}
func (e *Engine) syncWorkflowInputToTemplateCtx(executer *workflows.ProtocolExecuterPair, input *contextargs.Context) {
if executer == nil || executer.Options == nil || input == nil || !input.HasArgs() {
return
}
workflowValues := input.GetAll()
if len(workflowValues) == 0 {
return
}
executer.Options.GetTemplateCtx(input.MetaInput).Merge(workflowValues)
}