Skip to content

Commit 6a6fa4d

Browse files
authored
feat(fuzz): eval variables (#6358)
* feat(fuzz): eval vars for rule keys & values Signed-off-by: Dwi Siswanto <git@dw1.io> * chore: re-fmt fuzzing/dast errors Signed-off-by: Dwi Siswanto <git@dw1.io> * test(fuzz): adds `TestEvaluateVariables` Signed-off-by: Dwi Siswanto <git@dw1.io> --------- Signed-off-by: Dwi Siswanto <git@dw1.io>
1 parent 9fcacd0 commit 6a6fa4d

File tree

3 files changed

+292
-10
lines changed

3 files changed

+292
-10
lines changed

pkg/fuzz/execute.go

Lines changed: 65 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs"
1515
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/expressions"
1616
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/generators"
17+
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/marker"
1718
"github.com/projectdiscovery/nuclei/v3/pkg/utils/json"
1819
"github.com/projectdiscovery/retryablehttp-go"
1920
errorutil "github.com/projectdiscovery/utils/errors"
@@ -23,7 +24,7 @@ import (
2324
)
2425

2526
var (
26-
ErrRuleNotApplicable = errorutil.NewWithFmt("rule not applicable : %v")
27+
ErrRuleNotApplicable = errorutil.NewWithFmt("rule not applicable: %v")
2728
)
2829

2930
// IsErrRuleNotApplicable checks if an error is due to rule not applicable
@@ -189,6 +190,33 @@ mainLoop:
189190
return nil
190191
}
191192

193+
// evaluateVars evaluates variables in a string using available executor options
194+
func (rule *Rule) evaluateVars(input string) (string, error) {
195+
if rule.options == nil {
196+
return input, nil
197+
}
198+
199+
data := generators.MergeMaps(
200+
rule.options.Variables.GetAll(),
201+
rule.options.Constants,
202+
rule.options.Options.Vars.AsMap(),
203+
)
204+
205+
exprs := expressions.FindExpressions(input, marker.ParenthesisOpen, marker.ParenthesisClose, data)
206+
207+
err := expressions.ContainsUnresolvedVariables(exprs...)
208+
if err != nil {
209+
return input, err
210+
}
211+
212+
eval, err := expressions.Evaluate(input, data)
213+
if err != nil {
214+
return input, err
215+
}
216+
217+
return eval, nil
218+
}
219+
192220
// evaluateVarsWithInteractsh evaluates the variables with Interactsh URLs and updates them accordingly.
193221
func (rule *Rule) evaluateVarsWithInteractsh(data map[string]interface{}, interactshUrls []string) (map[string]interface{}, []string) {
194222
// Check if Interactsh options are configured
@@ -341,23 +369,47 @@ func (rule *Rule) Compile(generator *generators.PayloadGenerator, options *proto
341369
if len(rule.Keys) > 0 {
342370
rule.keysMap = make(map[string]struct{})
343371
}
372+
373+
// eval vars in "keys"
344374
for _, key := range rule.Keys {
345-
rule.keysMap[strings.ToLower(key)] = struct{}{}
375+
evaluatedKey, err := rule.evaluateVars(key)
376+
if err != nil {
377+
return errors.Wrap(err, "could not evaluate key")
378+
}
379+
380+
rule.keysMap[strings.ToLower(evaluatedKey)] = struct{}{}
346381
}
382+
383+
// eval vars in "values"
347384
for _, value := range rule.ValuesRegex {
348-
compiled, err := regexp.Compile(value)
385+
evaluatedValue, err := rule.evaluateVars(value)
386+
if err != nil {
387+
return errors.Wrap(err, "could not evaluate value regex")
388+
}
389+
390+
compiled, err := regexp.Compile(evaluatedValue)
349391
if err != nil {
350392
return errors.Wrap(err, "could not compile value regex")
351393
}
394+
352395
rule.valuesRegex = append(rule.valuesRegex, compiled)
353396
}
397+
398+
// eval vars in "keys-regex"
354399
for _, value := range rule.KeysRegex {
355-
compiled, err := regexp.Compile(value)
400+
evaluatedValue, err := rule.evaluateVars(value)
401+
if err != nil {
402+
return errors.Wrap(err, "could not evaluate key regex")
403+
}
404+
405+
compiled, err := regexp.Compile(evaluatedValue)
356406
if err != nil {
357407
return errors.Wrap(err, "could not compile key regex")
358408
}
409+
359410
rule.keysRegex = append(rule.keysRegex, compiled)
360411
}
412+
361413
if rule.ruleType != replaceRegexRuleType {
362414
if rule.ReplaceRegex != "" {
363415
return errors.Errorf("replace-regex is only applicable for replace and replace-regex rule types")
@@ -366,11 +418,19 @@ func (rule *Rule) Compile(generator *generators.PayloadGenerator, options *proto
366418
if rule.ReplaceRegex == "" {
367419
return errors.Errorf("replace-regex is required for replace-regex rule type")
368420
}
369-
compiled, err := regexp.Compile(rule.ReplaceRegex)
421+
422+
evalReplaceRegex, err := rule.evaluateVars(rule.ReplaceRegex)
423+
if err != nil {
424+
return errors.Wrap(err, "could not evaluate replace regex")
425+
}
426+
427+
compiled, err := regexp.Compile(evalReplaceRegex)
370428
if err != nil {
371429
return errors.Wrap(err, "could not compile replace regex")
372430
}
431+
373432
rule.replaceRegex = compiled
374433
}
434+
375435
return nil
376436
}

pkg/fuzz/fuzz_test.go

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ package fuzz
33
import (
44
"testing"
55

6+
"github.com/projectdiscovery/goflags"
7+
"github.com/projectdiscovery/nuclei/v3/pkg/protocols"
8+
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/variables"
9+
"github.com/projectdiscovery/nuclei/v3/pkg/types"
10+
"github.com/projectdiscovery/nuclei/v3/pkg/utils"
611
"github.com/stretchr/testify/require"
712
)
813

@@ -37,3 +42,219 @@ func TestRuleMatchKeyOrValue(t *testing.T) {
3742
require.False(t, result, "could not get correct result")
3843
})
3944
}
45+
46+
func TestEvaluateVariables(t *testing.T) {
47+
t.Run("keys", func(t *testing.T) {
48+
rule := &Rule{
49+
Keys: []string{"{{foo_var}}"},
50+
Part: "query",
51+
}
52+
53+
// mock
54+
templateVars := variables.Variable{
55+
InsertionOrderedStringMap: *utils.NewEmptyInsertionOrderedStringMap(1),
56+
}
57+
templateVars.Set("foo_var", "foo_var_value")
58+
59+
constants := map[string]interface{}{
60+
"const_key": "const_value",
61+
}
62+
63+
options := &types.Options{}
64+
65+
// runtime vars (to simulate CLI)
66+
runtimeVars := goflags.RuntimeMap{}
67+
_ = runtimeVars.Set("runtime_key=runtime_value")
68+
options.Vars = runtimeVars
69+
70+
executorOpts := &protocols.ExecutorOptions{
71+
Variables: templateVars,
72+
Constants: constants,
73+
Options: options,
74+
}
75+
76+
err := rule.Compile(nil, executorOpts)
77+
require.NoError(t, err, "could not compile rule")
78+
79+
result := rule.matchKeyOrValue("foo_var_value", "test_value")
80+
require.True(t, result, "should match evaluated variable key")
81+
82+
result = rule.matchKeyOrValue("{{foo_var}}", "test_value")
83+
require.False(t, result, "should not match unevaluated variable key")
84+
})
85+
86+
t.Run("keys-regex", func(t *testing.T) {
87+
rule := &Rule{
88+
KeysRegex: []string{"^{{foo_var}}"},
89+
Part: "query",
90+
}
91+
92+
templateVars := variables.Variable{
93+
InsertionOrderedStringMap: *utils.NewEmptyInsertionOrderedStringMap(1),
94+
}
95+
templateVars.Set("foo_var", "foo_var_value")
96+
97+
executorOpts := &protocols.ExecutorOptions{
98+
Variables: templateVars,
99+
Constants: map[string]interface{}{},
100+
Options: &types.Options{},
101+
}
102+
103+
err := rule.Compile(nil, executorOpts)
104+
require.NoError(t, err, "could not compile rule")
105+
106+
result := rule.matchKeyOrValue("foo_var_value", "test_value")
107+
require.True(t, result, "should match evaluated variable in regex")
108+
109+
result = rule.matchKeyOrValue("other_key", "test_value")
110+
require.False(t, result, "should not match non-matching key")
111+
})
112+
113+
t.Run("values-regex", func(t *testing.T) {
114+
rule := &Rule{
115+
ValuesRegex: []string{"{{foo_var}}"},
116+
Part: "query",
117+
}
118+
119+
templateVars := variables.Variable{
120+
InsertionOrderedStringMap: *utils.NewEmptyInsertionOrderedStringMap(1),
121+
}
122+
templateVars.Set("foo_var", "test_pattern")
123+
124+
executorOpts := &protocols.ExecutorOptions{
125+
Variables: templateVars,
126+
Constants: map[string]interface{}{},
127+
Options: &types.Options{},
128+
}
129+
130+
err := rule.Compile(nil, executorOpts)
131+
require.NoError(t, err, "could not compile rule")
132+
133+
result := rule.matchKeyOrValue("test_key", "test_pattern")
134+
require.True(t, result, "should match evaluated variable in values regex")
135+
136+
result = rule.matchKeyOrValue("test_key", "other_value")
137+
require.False(t, result, "should not match non-matching value")
138+
})
139+
140+
// complex vars w/ consts and runtime vars
141+
t.Run("complex-variables", func(t *testing.T) {
142+
rule := &Rule{
143+
Keys: []string{"{{template_var}}", "{{const_key}}", "{{runtime_key}}"},
144+
Part: "query",
145+
}
146+
147+
templateVars := variables.Variable{
148+
InsertionOrderedStringMap: *utils.NewEmptyInsertionOrderedStringMap(1),
149+
}
150+
templateVars.Set("template_var", "template_value")
151+
152+
constants := map[string]interface{}{
153+
"const_key": "const_value",
154+
}
155+
156+
options := &types.Options{}
157+
runtimeVars := goflags.RuntimeMap{}
158+
_ = runtimeVars.Set("runtime_key=runtime_value")
159+
options.Vars = runtimeVars
160+
161+
executorOpts := &protocols.ExecutorOptions{
162+
Variables: templateVars,
163+
Constants: constants,
164+
Options: options,
165+
}
166+
167+
err := rule.Compile(nil, executorOpts)
168+
require.NoError(t, err, "could not compile rule")
169+
170+
result := rule.matchKeyOrValue("template_value", "test")
171+
require.True(t, result, "should match template variable")
172+
173+
result = rule.matchKeyOrValue("const_value", "test")
174+
require.True(t, result, "should match constant")
175+
176+
result = rule.matchKeyOrValue("runtime_value", "test")
177+
require.True(t, result, "should match runtime variable")
178+
179+
result = rule.matchKeyOrValue("{{template_var}}", "test")
180+
require.False(t, result, "should not match unevaluated template variable")
181+
})
182+
183+
t.Run("invalid-variables", func(t *testing.T) {
184+
rule := &Rule{
185+
Keys: []string{"{{nonexistent_var}}"},
186+
Part: "query",
187+
}
188+
189+
executorOpts := &protocols.ExecutorOptions{
190+
Variables: variables.Variable{
191+
InsertionOrderedStringMap: *utils.NewEmptyInsertionOrderedStringMap(0),
192+
},
193+
Constants: map[string]interface{}{},
194+
Options: &types.Options{},
195+
}
196+
197+
err := rule.Compile(nil, executorOpts)
198+
if err != nil {
199+
require.Contains(t, err.Error(), "unresolved", "error should mention unresolved variables")
200+
} else {
201+
result := rule.matchKeyOrValue("some_key", "some_value")
202+
require.False(t, result, "should not match when variables are unresolved")
203+
}
204+
})
205+
206+
t.Run("evaluateVars-function", func(t *testing.T) {
207+
rule := &Rule{}
208+
209+
templateVars := variables.Variable{
210+
InsertionOrderedStringMap: *utils.NewEmptyInsertionOrderedStringMap(1),
211+
}
212+
templateVars.Set("test_var", "test_value")
213+
214+
constants := map[string]interface{}{
215+
"const_var": "const_value",
216+
}
217+
218+
options := &types.Options{}
219+
runtimeVars := goflags.RuntimeMap{}
220+
_ = runtimeVars.Set("runtime_var=runtime_value")
221+
options.Vars = runtimeVars
222+
223+
executorOpts := &protocols.ExecutorOptions{
224+
Variables: templateVars,
225+
Constants: constants,
226+
Options: options,
227+
}
228+
229+
rule.options = executorOpts
230+
231+
// Test simple var substitution
232+
result, err := rule.evaluateVars("{{test_var}}")
233+
require.NoError(t, err, "should evaluate template variable")
234+
require.Equal(t, "test_value", result, "should return evaluated value")
235+
236+
// Test constant substitution
237+
result, err = rule.evaluateVars("{{const_var}}")
238+
require.NoError(t, err, "should evaluate constant")
239+
require.Equal(t, "const_value", result, "should return constant value")
240+
241+
// Test runtime var substitution
242+
result, err = rule.evaluateVars("{{runtime_var}}")
243+
require.NoError(t, err, "should evaluate runtime variable")
244+
require.Equal(t, "runtime_value", result, "should return runtime value")
245+
246+
// Test mixed content
247+
result, err = rule.evaluateVars("prefix-{{test_var}}-suffix")
248+
require.NoError(t, err, "should evaluate mixed content")
249+
require.Equal(t, "prefix-test_value-suffix", result, "should return mixed evaluated content")
250+
251+
// Test unresolved var - should either fail during evaluation or return original string
252+
result2, err := rule.evaluateVars("{{nonexistent}}")
253+
if err != nil {
254+
require.Contains(t, err.Error(), "unresolved", "should fail for unresolved variable")
255+
} else {
256+
// If no error, it should return the original unresolved variable
257+
require.Equal(t, "{{nonexistent}}", result2, "should return original string for unresolved variable")
258+
}
259+
})
260+
}

pkg/protocols/http/request_fuzz.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ func (request *Request) executeFuzzingRule(input *contextargs.Context, previous
7676
if errors.Is(err, ErrMissingVars) {
7777
return err
7878
}
79-
gologger.Verbose().Msgf("[%s] fuzz: payload request execution failed : %s\n", request.options.TemplateID, err)
79+
gologger.Verbose().Msgf("[%s] fuzz: payload request execution failed: %s\n", request.options.TemplateID, err)
8080
}
8181
return nil
8282
}
@@ -103,13 +103,13 @@ func (request *Request) executeFuzzingRule(input *contextargs.Context, previous
103103
// in case of any error, return it
104104
if fuzz.IsErrRuleNotApplicable(err) {
105105
// log and fail silently
106-
gologger.Verbose().Msgf("[%s] fuzz: rule not applicable : %s\n", request.options.TemplateID, err)
106+
gologger.Verbose().Msgf("[%s] fuzz: %s\n", request.options.TemplateID, err)
107107
return nil
108108
}
109109
if errors.Is(err, ErrMissingVars) {
110110
return err
111111
}
112-
gologger.Verbose().Msgf("[%s] fuzz: payload request execution failed : %s\n", request.options.TemplateID, err)
112+
gologger.Verbose().Msgf("[%s] fuzz: payload request execution failed: %s\n", request.options.TemplateID, err)
113113
}
114114
return nil
115115
}
@@ -158,7 +158,7 @@ func (request *Request) executeAllFuzzingRules(input *contextargs.Context, value
158158
continue
159159
}
160160
if fuzz.IsErrRuleNotApplicable(err) {
161-
gologger.Verbose().Msgf("[%s] fuzz: rule not applicable : %s\n", request.options.TemplateID, err)
161+
gologger.Verbose().Msgf("[%s] fuzz: %s\n", request.options.TemplateID, err)
162162
continue
163163
}
164164
if err == types.ErrNoMoreRequests {
@@ -168,8 +168,9 @@ func (request *Request) executeAllFuzzingRules(input *contextargs.Context, value
168168
}
169169

170170
if !applicable {
171-
return fuzz.ErrRuleNotApplicable.Msgf(fmt.Sprintf("no rule was applicable for this request: %v", input.MetaInput.Input))
171+
return fmt.Errorf("no rule was applicable for this request: %v", input.MetaInput.Input)
172172
}
173+
173174
return nil
174175
}
175176

0 commit comments

Comments
 (0)