Path: blob/dev/pkg/protocols/common/variables/variables_test.go
2843 views
package variables12import (3"testing"4"time"56"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/interactsh"7"github.com/projectdiscovery/nuclei/v3/pkg/utils"8"github.com/projectdiscovery/nuclei/v3/pkg/utils/json"9"github.com/stretchr/testify/require"10"gopkg.in/yaml.v2"11)1213func TestVariablesEvaluate(t *testing.T) {14data := `a2: "{{md5('test')}}"15a3: "this_is_random_text"16a4: "{{date_time('%Y-%M-%D')}}"17a5: "{{reverse(hostname)}}"18a6: "123456"`1920variables := Variable{}21err := yaml.Unmarshal([]byte(data), &variables)22require.NoError(t, err, "could not unmarshal variables")2324result := variables.Evaluate(map[string]interface{}{"hostname": "google.com"})25a4 := time.Now().Format("2006-01-02")26require.Equal(t, map[string]interface{}{"a2": "098f6bcd4621d373cade4e832627b4f6", "a3": "this_is_random_text", "a4": a4, "a5": "moc.elgoog", "a6": "123456"}, result, "could not get correct elements")2728// json29data = `{30"a2": "{{md5('test')}}",31"a3": "this_is_random_text",32"a4": "{{date_time('%Y-%M-%D')}}",33"a5": "{{reverse(hostname)}}",34"a6": "123456"35}`36variables = Variable{}37err = json.Unmarshal([]byte(data), &variables)38require.NoError(t, err, "could not unmarshal json variables")3940result = variables.Evaluate(map[string]interface{}{"hostname": "google.com"})41a4 = time.Now().Format("2006-01-02")42require.Equal(t, map[string]interface{}{"a2": "098f6bcd4621d373cade4e832627b4f6", "a3": "this_is_random_text", "a4": a4, "a5": "moc.elgoog", "a6": "123456"}, result, "could not get correct elements")4344}4546func TestCheckForLazyEval(t *testing.T) {47t.Run("undefined-parameters-in-expression", func(t *testing.T) {48// Variables with expressions that reference undefined parameters49// should be marked for lazy evaluation50variables := &Variable{51InsertionOrderedStringMap: *utils.NewEmptyInsertionOrderedStringMap(2),52}53variables.Set("var1", "{{sha1(serial)}}") // 'serial' is undefined54variables.Set("var2", "{{replace(user, '.', '')}}") // 'user' is undefined5556result := variables.checkForLazyEval()57require.True(t, result, "should detect undefined parameters and set LazyEval=true")58require.True(t, variables.LazyEval, "LazyEval flag should be true")59})6061t.Run("self-referencing-variables", func(t *testing.T) {62// Variables that reference other defined variables should NOT be lazy63variables := &Variable{64InsertionOrderedStringMap: *utils.NewEmptyInsertionOrderedStringMap(2),65}66variables.Set("base", "example")67variables.Set("derived", "{{base}}_suffix") // 'base' is defined6869result := variables.checkForLazyEval()70require.False(t, result, "should not set LazyEval for self-referencing defined variables")71require.False(t, variables.LazyEval, "LazyEval flag should be false")72})7374t.Run("constant-expressions", func(t *testing.T) {75// Constant expressions without variables should NOT be lazy76variables := &Variable{77InsertionOrderedStringMap: *utils.NewEmptyInsertionOrderedStringMap(2),78}79variables.Set("const1", "{{2+2}}")80variables.Set("const2", "{{rand_int(1, 100)}}")8182result := variables.checkForLazyEval()83require.False(t, result, "should not set LazyEval for constant expressions")84require.False(t, variables.LazyEval, "LazyEval flag should be false")85})8687t.Run("known-runtime-variables", func(t *testing.T) {88// Variables with known runtime variables (Host, BaseURL, etc.) should be lazy89variables := &Variable{90InsertionOrderedStringMap: *utils.NewEmptyInsertionOrderedStringMap(1),91}92variables.Set("url", "{{BaseURL}}/api")9394result := variables.checkForLazyEval()95require.True(t, result, "should detect known runtime variables")96require.True(t, variables.LazyEval, "LazyEval flag should be true")97})9899t.Run("interactsh-url", func(t *testing.T) {100// Variables with interactsh-url should be lazy101variables := &Variable{102InsertionOrderedStringMap: *utils.NewEmptyInsertionOrderedStringMap(1),103}104variables.Set("callback", "{{interactsh-url}}")105106result := variables.checkForLazyEval()107require.True(t, result, "should detect interactsh-url")108require.True(t, variables.LazyEval, "LazyEval flag should be true")109})110111t.Run("mixed-defined-and-undefined", func(t *testing.T) {112// Mix of defined and undefined parameters in actual expressions113variables := &Variable{114InsertionOrderedStringMap: *utils.NewEmptyInsertionOrderedStringMap(3),115}116variables.Set("defined", "value")117variables.Set("uses_defined", "{{base64(defined)}}") // OK - 'defined' exists118variables.Set("uses_undefined", "{{base64(undefined_param)}}") // NOT OK - 'undefined_param' doesn't exist119120result := variables.checkForLazyEval()121require.True(t, result, "should detect undefined parameters even with some defined")122require.True(t, variables.LazyEval, "LazyEval flag should be true")123})124125t.Run("plain-strings-no-expressions", func(t *testing.T) {126// Plain string values without expressions127variables := &Variable{128InsertionOrderedStringMap: *utils.NewEmptyInsertionOrderedStringMap(2),129}130variables.Set("plain1", "simple value")131variables.Set("plain2", "another value")132133result := variables.checkForLazyEval()134require.False(t, result, "should not set LazyEval for plain strings")135require.False(t, variables.LazyEval, "LazyEval flag should be false")136})137138t.Run("complex-expression-with-undefined", func(t *testing.T) {139// Complex expression with multiple undefined parameters140variables := &Variable{141InsertionOrderedStringMap: *utils.NewEmptyInsertionOrderedStringMap(1),142}143variables.Set("complex", "{{sha1(cert_serial + issuer)}}")144145result := variables.checkForLazyEval()146require.True(t, result, "should detect undefined parameters in complex expressions")147require.True(t, variables.LazyEval, "LazyEval flag should be true")148})149}150151func TestVariablesEvaluateChained(t *testing.T) {152t.Run("chained-variable-references", func(t *testing.T) {153// Test that variables can reference previously defined variables154// and that input values (like BaseURL) are available for evaluation155// but not included in the result156variables := &Variable{157LazyEval: true, // skip auto-evaluation in UnmarshalYAML158InsertionOrderedStringMap: *utils.NewEmptyInsertionOrderedStringMap(3),159}160variables.Set("a", "hello")161variables.Set("b", "{{a}} world")162variables.Set("c", "{{b}}!")163164inputValues := map[string]interface{}{165"BaseURL": "http://example.com",166"Host": "example.com",167}168169result := variables.Evaluate(inputValues)170171// Result should contain only the defined variables, not input values172require.Len(t, result, 3, "result should contain exactly 3 variables")173require.NotContains(t, result, "BaseURL", "result should not contain input values")174require.NotContains(t, result, "Host", "result should not contain input values")175176// Chained evaluation should work correctly177require.Equal(t, "hello", result["a"])178require.Equal(t, "hello world", result["b"])179require.Equal(t, "hello world!", result["c"])180})181182t.Run("variables-using-input-values", func(t *testing.T) {183// Test that variables can use input values in expressions184variables := &Variable{185LazyEval: true,186InsertionOrderedStringMap: *utils.NewEmptyInsertionOrderedStringMap(2),187}188variables.Set("api_url", "{{BaseURL}}/api/v1")189variables.Set("full_path", "{{api_url}}/users")190191inputValues := map[string]interface{}{192"BaseURL": "http://example.com",193}194195result := variables.Evaluate(inputValues)196197require.Len(t, result, 2)198require.Equal(t, "http://example.com/api/v1", result["api_url"])199require.Equal(t, "http://example.com/api/v1/users", result["full_path"])200require.NotContains(t, result, "BaseURL")201})202203t.Run("mixed-expressions-and-chaining", func(t *testing.T) {204// Test combining DSL functions with chained variables205variables := &Variable{206LazyEval: true,207InsertionOrderedStringMap: *utils.NewEmptyInsertionOrderedStringMap(3),208}209variables.Set("token", "secret123")210variables.Set("hashed", "{{md5(token)}}")211variables.Set("header", "X-Auth: {{hashed}}")212213result := variables.Evaluate(map[string]interface{}{})214215require.Equal(t, "secret123", result["token"])216require.Equal(t, "5d7845ac6ee7cfffafc5fe5f35cf666d", result["hashed"]) // md5("secret123")217require.Equal(t, "X-Auth: 5d7845ac6ee7cfffafc5fe5f35cf666d", result["header"])218})219220t.Run("evaluation-order-preserved", func(t *testing.T) {221// Test that evaluation follows insertion order222// (important for variables that depend on previously defined ones)223variables := &Variable{224LazyEval: true,225InsertionOrderedStringMap: *utils.NewEmptyInsertionOrderedStringMap(4),226}227variables.Set("step1", "A")228variables.Set("step2", "{{step1}}B")229variables.Set("step3", "{{step2}}C")230variables.Set("step4", "{{step3}}D")231232result := variables.Evaluate(map[string]interface{}{})233234require.Equal(t, "A", result["step1"])235require.Equal(t, "AB", result["step2"])236require.Equal(t, "ABC", result["step3"])237require.Equal(t, "ABCD", result["step4"])238})239}240241func TestEvaluateWithInteractshOverrideOrder(t *testing.T) {242// This test demonstrates a bug where interactsh URL replacement is wasted243// when an input value exists for the same variable key.244//245// Bug scenario:246// 1. Variable "callback" is defined with "{{interactsh-url}}"247// 2. Input values contain "callback" with some other value248// 3. The interactsh-url is replaced first (wasting an interactsh URL)249// 4. Then immediately overwritten by the input value250//251// Expected behavior: Input override should be checked FIRST, then interactsh252// replacement should happen on the final valueString.253254t.Run("interactsh-replacement-with-input-override", func(t *testing.T) {255variables := &Variable{256LazyEval: true,257InsertionOrderedStringMap: *utils.NewEmptyInsertionOrderedStringMap(1),258}259variables.Set("callback", "{{interactsh-url}}")260261// Input provides an override that also contains interactsh-url262inputValues := map[string]interface{}{263"callback": "https://custom.{{interactsh-url}}/path",264}265266// Create a real interactsh client for testing267client, err := interactsh.New(&interactsh.Options{268ServerURL: "oast.fun",269CacheSize: 100,270Eviction: 60 * time.Second,271CooldownPeriod: 5 * time.Second,272PollDuration: 5 * time.Second,273DisableHttpFallback: true,274})275require.NoError(t, err, "could not create interactsh client")276defer client.Close()277278result, urls := variables.EvaluateWithInteractsh(inputValues, client)279280// The input override contains interactsh-url, so it should be replaced281// and we should have exactly 1 URL from the input override282require.Len(t, urls, 1, "should have 1 interactsh URL from input override")283284// The result should use the input override (with interactsh replaced)285require.Contains(t, result["callback"], "https://custom.", "should use input override pattern")286require.Contains(t, result["callback"], "/path", "should use input override pattern")287require.NotContains(t, result["callback"], "{{interactsh-url}}", "interactsh should be replaced")288})289290t.Run("interactsh-replacement-without-input-override", func(t *testing.T) {291variables := &Variable{292LazyEval: true,293InsertionOrderedStringMap: *utils.NewEmptyInsertionOrderedStringMap(1),294}295variables.Set("callback", "{{interactsh-url}}")296297// No input override for "callback"298inputValues := map[string]interface{}{299"other_key": "other_value",300}301302client, err := interactsh.New(&interactsh.Options{303ServerURL: "oast.fun",304CacheSize: 100,305Eviction: 60 * time.Second,306CooldownPeriod: 5 * time.Second,307PollDuration: 5 * time.Second,308DisableHttpFallback: true,309})310require.NoError(t, err, "could not create interactsh client")311defer client.Close()312313result, urls := variables.EvaluateWithInteractsh(inputValues, client)314315// Should have 1 URL from the variable definition316require.Len(t, urls, 1, "should have 1 interactsh URL")317318// The result should be the replaced interactsh URL319require.NotContains(t, result["callback"], "{{interactsh-url}}", "interactsh should be replaced")320require.NotEmpty(t, result["callback"], "callback should have a value")321})322}323324325