Skip to content

Commit 4177a48

Browse files
Merge pull request #7298 from projectdiscovery/7295-dynamic-auth-flow-fix
fixing flow with auth
2 parents c8a9853 + ab0199b commit 4177a48

File tree

3 files changed

+672
-9
lines changed

3 files changed

+672
-9
lines changed

internal/runner/lazy.go

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -109,11 +109,9 @@ func GetLazyAuthFetchCallback(opts *AuthLazyFetchOptions) authx.LazyFetchSecret
109109
var finalErr error
110110
ctx.OnResult = func(e *output.InternalWrappedEvent) {
111111
if e == nil {
112-
finalErr = fmt.Errorf("no result found for template: %s", d.TemplatePath)
113112
return
114113
}
115114
if !e.HasOperatorResult() {
116-
finalErr = fmt.Errorf("no result found for template: %s", d.TemplatePath)
117115
return
118116
}
119117
// dynamic values
@@ -133,20 +131,16 @@ func GetLazyAuthFetchCallback(opts *AuthLazyFetchOptions) authx.LazyFetchSecret
133131
data[k] = v[0]
134132
}
135133
}
136-
if len(data) == 0 {
137-
if e.OperatorsResult.Matched {
138-
finalErr = fmt.Errorf("match found but no (dynamic/extracted) values found for template: %s", d.TemplatePath)
139-
} else {
140-
finalErr = fmt.Errorf("no match or (dynamic/extracted) values found for template: %s", d.TemplatePath)
141-
}
142-
}
143134
// log result of template in result file/screen
144135
_ = writer.WriteResult(e, opts.ExecOpts.Output, opts.ExecOpts.Progress, opts.ExecOpts.IssuesClient)
145136
}
146137
_, execErr := tmpl.Executer.ExecuteWithResults(ctx)
147138
if execErr != nil {
148139
finalErr = execErr
149140
}
141+
if finalErr == nil && len(data) == 0 {
142+
finalErr = fmt.Errorf("no extracted values found for template: %s", d.TemplatePath)
143+
}
150144
// store extracted result in auth context
151145
d.Extracted = data
152146
if finalErr != nil && opts.OnError != nil {

internal/runner/lazy_test.go

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
package runner
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
7+
"github.com/projectdiscovery/nuclei/v3/pkg/operators"
8+
"github.com/projectdiscovery/nuclei/v3/pkg/output"
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
// simulateOnResultCallback reproduces the OnResult callback from lazy.go.
13+
// It silently skips events without operator results and only errors post-execution
14+
// if no data was extracted at all.
15+
func simulateOnResultCallback(templatePath string, events []*output.InternalWrappedEvent) (map[string]interface{}, error) {
16+
data := map[string]interface{}{}
17+
18+
for _, e := range events {
19+
if e == nil {
20+
continue
21+
}
22+
if !e.HasOperatorResult() {
23+
continue
24+
}
25+
for k, v := range e.OperatorsResult.DynamicValues {
26+
for _, value := range v {
27+
oldVal, ok := data[k]
28+
if !ok || len(value) > len(oldVal.(string)) {
29+
data[k] = value
30+
}
31+
}
32+
}
33+
for k, v := range e.OperatorsResult.Extracts {
34+
if len(v) > 0 {
35+
data[k] = v[0]
36+
}
37+
}
38+
}
39+
40+
var finalErr error
41+
if len(data) == 0 {
42+
finalErr = fmt.Errorf("no extracted values found for template: %s", templatePath)
43+
}
44+
return data, finalErr
45+
}
46+
47+
// flowEvents returns a pair of events simulating a flow: http(1) && http(2)
48+
// where http(1) has no matchers and http(2) has extractors.
49+
func flowEvents() []*output.InternalWrappedEvent {
50+
return []*output.InternalWrappedEvent{
51+
{InternalEvent: map[string]interface{}{"status_code": 200}},
52+
{
53+
OperatorsResult: &operators.Result{
54+
Matched: true,
55+
Extracts: map[string][]string{
56+
"userid": {"USER-042"},
57+
"customerid": {"CUST-001"},
58+
},
59+
},
60+
InternalEvent: map[string]interface{}{"status_code": 200},
61+
},
62+
}
63+
}
64+
65+
// TestCallbackWorksWithFlowTemplate verifies that the OnResult callback
66+
// correctly handles flow templates (e.g., flow: http(1) && http(2)) where
67+
// the first request has no matchers/extractors and the second has extractors.
68+
// Regression test for #7295.
69+
func TestCallbackWorksWithFlowTemplate(t *testing.T) {
70+
data, err := simulateOnResultCallback("login.yaml", flowEvents())
71+
72+
require.Equal(t, "USER-042", data["userid"])
73+
require.Equal(t, "CUST-001", data["customerid"])
74+
require.NoError(t, err, "should not error when data is extracted from a later event")
75+
}
76+
77+
// TestCallbackSingleRequest verifies single-request templates still work.
78+
func TestCallbackSingleRequest(t *testing.T) {
79+
events := []*output.InternalWrappedEvent{
80+
{
81+
OperatorsResult: &operators.Result{
82+
Matched: true,
83+
Extracts: map[string][]string{
84+
"token": {"abc123"},
85+
},
86+
},
87+
InternalEvent: map[string]interface{}{},
88+
},
89+
}
90+
91+
data, err := simulateOnResultCallback("auth.yaml", events)
92+
93+
require.NoError(t, err)
94+
require.Equal(t, "abc123", data["token"])
95+
}
96+
97+
// TestCallbackNoExtraction verifies error is returned when nothing is extracted.
98+
func TestCallbackNoExtraction(t *testing.T) {
99+
events := []*output.InternalWrappedEvent{
100+
{InternalEvent: map[string]interface{}{"status_code": 404}},
101+
}
102+
103+
data, err := simulateOnResultCallback("auth.yaml", events)
104+
105+
require.Error(t, err)
106+
require.Contains(t, err.Error(), "no extracted values")
107+
require.Empty(t, data)
108+
}
109+
110+
// TestCallbackNilEvent verifies nil events are safely ignored.
111+
func TestCallbackNilEvent(t *testing.T) {
112+
events := []*output.InternalWrappedEvent{nil}
113+
114+
data, err := simulateOnResultCallback("auth.yaml", events)
115+
116+
require.Error(t, err)
117+
require.Contains(t, err.Error(), "no extracted values")
118+
require.Empty(t, data)
119+
}
120+
121+
// TestCallbackDynamicValues verifies dynamic values (internal extractors) are captured.
122+
func TestCallbackDynamicValues(t *testing.T) {
123+
events := []*output.InternalWrappedEvent{
124+
{
125+
OperatorsResult: &operators.Result{
126+
Matched: true,
127+
DynamicValues: map[string][]string{
128+
"session": {"sess-xyz-123", "sess-a"},
129+
},
130+
},
131+
InternalEvent: map[string]interface{}{},
132+
},
133+
}
134+
135+
data, err := simulateOnResultCallback("auth.yaml", events)
136+
137+
require.NoError(t, err)
138+
require.Equal(t, "sess-xyz-123", data["session"], "should pick the longest value")
139+
}

0 commit comments

Comments
 (0)