VYPR
Medium severity5.3NVD Advisory· Published May 8, 2026· Updated May 8, 2026

CVE-2026-41645

CVE-2026-41645

Description

Nuclei is a vulnerability scanner built on a simple YAML-based DSL. From version 3.0.0 to before version 3.8.0, a vulnerability in Nuclei's expression evaluation engine makes it possible for a malicious target server to inject and execute supported DSL expressions. This happens when HTTP response data containing helper/function syntax gets reused by multi-step templates. If the -env-vars / -ev option is explicitly enabled, this can expose host environment variables. That option is off by default, so standard configurations are not affected by the information disclosure risk. This issue has been patched in version 3.8.0.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
github.com/projectdiscovery/nuclei/v3Go
>= 3.0.0, < 3.8.03.8.0

Affected products

2
  • Projectdiscovery/Nucleiinferred2 versions
    <3.8.0+ 1 more
    • (no CPE)range: <3.8.0
    • cpe:2.3:a:projectdiscovery:nuclei:*:*:*:*:*:go:*:*range: >=3.0.0,<3.8.0

Patches

2
6c803c74d193

fix(expressions): avoid helper eval in literal checks (#7321)

https://github.com/projectdiscovery/nucleiDwi SiswantoApr 10, 2026via ghsa
6 files changed · +142 17
  • pkg/protocols/common/expressions/expressions.go+25 13 modified
    @@ -1,10 +1,10 @@
     package expressions
     
     import (
    +	"fmt"
     	"strings"
     
     	"github.com/Knetic/govaluate"
    -	"github.com/projectdiscovery/gologger"
     
     	"github.com/projectdiscovery/nuclei/v3/pkg/operators/common/dsl"
     	"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/marker"
    @@ -53,28 +53,40 @@ func evaluate(data string, base map[string]interface{}) (string, error) {
     	// - complex: containing helper functions [ + variables]
     	// literals like {{2+2}} are not considered expressions
     	for _, expression := range expressions {
    +		originalExpression := expression
     		// replace variable placeholders with base values
     		expression = replacer.Replace(expression, base)
    +
     		// turns expressions (either helper functions+base values or base values)
     		compiled, err := govaluate.NewEvaluableExpressionWithFunctions(expression, dsl.HelperFunctions)
     		if err != nil {
    -			gologger.Warning().Msgf("Failed to compile expression '%s': %v", expression, err)
    -			continue
    -		}
    -		// propagate unresolved {{...}} markers from variable values so the
    -		// downstream ContainsUnresolvedVariables check can detect them instead
    -		// of having encoding functions (e.g. base64) hide them
    -		if markers := unresolvedVarMarkers(compiled.Vars(), base); markers != "" {
    -			data = replacer.ReplaceOne(data, expression, markers)
    -			continue
    +			return data, fmt.Errorf("failed to compile expression %q: %w", originalExpression, err)
     		}
    +
     		result, err := compiled.Evaluate(base)
     		if err != nil {
    -			gologger.Warning().Msgf("Failed to evaluate expression '%s': %v", expression, err)
    -			continue
    +			return data, fmt.Errorf("failed to evaluate expression %q: %w", originalExpression, err)
     		}
    +
    +		replacement := result
    +		// Preserve unresolved markers only when a helper call would otherwise
    +		// hide them from downstream validation. Plain expressions such as
    +		// comparisons should evaluate normally.
    +		if markers := unresolvedVarMarkers(compiled.Vars(), base); markers != "" {
    +			usesFunctions := false
    +			for _, token := range compiled.Tokens() {
    +				if token.Kind == govaluate.FUNCTION {
    +					usesFunctions = true
    +					break
    +				}
    +			}
    +			if usesFunctions && ContainsUnresolvedVariables(fmt.Sprint(result)) == nil {
    +				replacement = markers
    +			}
    +		}
    +
     		// replace incrementally
    -		data = replacer.ReplaceOne(data, expression, result)
    +		data = replacer.ReplaceOne(data, expression, replacement)
     	}
     	return data, nil
     }
    
  • pkg/protocols/common/expressions/expressions_test.go+47 0 modified
    @@ -105,3 +105,50 @@ func TestEvaluateDoesNotReinterpretResolvedValues(t *testing.T) {
     		})
     	}
     }
    +
    +func TestEvaluateDoesNotExecuteHelpersFromResolvedValues(t *testing.T) {
    +	var calls int
    +
    +	withTestHelperFunction(t, "test_side_effect", func(args ...interface{}) (interface{}, error) {
    +		calls++
    +		return "ok", nil
    +	})
    +
    +	value, err := Evaluate("{{body}}", map[string]interface{}{
    +		"body": "{{test_side_effect(1)}}",
    +	})
    +	require.NoError(t, err)
    +	require.Equal(t, "{{test_side_effect(1)}}", value)
    +	require.Zero(t, calls)
    +}
    +
    +func TestEvaluateReturnsErrorForInvalidTemplateExpression(t *testing.T) {
    +	_, err := Evaluate("{{base64()}}", map[string]interface{}{})
    +	require.Error(t, err)
    +	require.ErrorContains(t, err, `failed to evaluate expression "base64()"`)
    +}
    +
    +func TestEvaluateErrorDoesNotLeakResolvedValues(t *testing.T) {
    +	_, err := Evaluate("{{base64('{{secret_token}}', 'extra')}}", map[string]interface{}{
    +		"secret_token": "top-secret-cia-mi6-kgb-mossad-classified",
    +	})
    +	require.Error(t, err)
    +	require.ErrorContains(t, err, `failed to evaluate expression "base64('{{secret_token}}', 'extra')"`)
    +	require.NotContains(t, err.Error(), "top-secret-cia-mi6-kgb-mossad-classified")
    +}
    +
    +func TestEvaluatePlainExpressionsWithMarkerLikeValues(t *testing.T) {
    +	value, err := Evaluate("{{body != ''}}", map[string]interface{}{
    +		"body": "{{contact_id}}",
    +	})
    +	require.NoError(t, err)
    +	require.Equal(t, "true", value)
    +}
    +
    +func TestEvaluatePreservesVisibleMarkersFromHelperResults(t *testing.T) {
    +	value, err := Evaluate("{{concat(body, '-x')}}", map[string]interface{}{
    +		"body": "{{contact_id}}",
    +	})
    +	require.NoError(t, err)
    +	require.Equal(t, "{{contact_id}}-x", value)
    +}
    
  • pkg/protocols/common/expressions/variables.go+5 4 modified
    @@ -120,9 +120,10 @@ func hasLiteralsOnly(data string) bool {
     	if err != nil {
     		return false
     	}
    -	if expr != nil {
    -		_, err = expr.Evaluate(nil)
    -		return err == nil
    +
    +	if expr == nil {
    +		return true
     	}
    -	return true
    +
    +	return len(expr.Vars()) == 0
     }
    
  • pkg/protocols/common/expressions/variables_test.go+29 0 modified
    @@ -4,9 +4,26 @@ import (
     	"errors"
     	"testing"
     
    +	"github.com/Knetic/govaluate"
    +	"github.com/projectdiscovery/nuclei/v3/pkg/operators/common/dsl"
     	"github.com/stretchr/testify/require"
     )
     
    +func withTestHelperFunction(t *testing.T, name string, fn govaluate.ExpressionFunction) {
    +	t.Helper()
    +
    +	originalFn, hadFn := dsl.HelperFunctions[name]
    +	dsl.HelperFunctions[name] = fn
    +
    +	t.Cleanup(func() {
    +		if hadFn {
    +			dsl.HelperFunctions[name] = originalFn
    +			return
    +		}
    +		delete(dsl.HelperFunctions, name)
    +	})
    +}
    +
     func TestUnresolvedVariablesCheck(t *testing.T) {
     	tests := []struct {
     		data string
    @@ -26,3 +43,15 @@ func TestUnresolvedVariablesCheck(t *testing.T) {
     		require.Equal(t, test.err, err, "could not get unresolved variables")
     	}
     }
    +
    +func TestUnresolvedVariablesCheckDoesNotExecuteHelpers(t *testing.T) {
    +	var calls int
    +	withTestHelperFunction(t, "test_side_effect", func(args ...interface{}) (interface{}, error) {
    +		calls++
    +		return "ok", nil
    +	})
    +
    +	err := ContainsUnresolvedVariables("{{test_side_effect(1)}}")
    +	require.NoError(t, err)
    +	require.Zero(t, calls)
    +}
    
  • pkg/protocols/javascript/js.go+1 0 modified
    @@ -701,6 +701,7 @@ func (request *Request) getArgsCopy(input *contextargs.Context, payloadValues ma
     	if err != nil {
     		requestOptions.Output.Request(requestOptions.TemplateID, input.MetaInput.Input, request.Type().String(), err)
     		requestOptions.Progress.IncrementFailedRequestsBy(1)
    +		return nil, err
     	}
     	// "Port" is a special variable that is considered as network port
     	// and is conditional based on input port and default port specified in input
    
  • pkg/protocols/javascript/js_test.go+35 0 modified
    @@ -9,8 +9,11 @@ import (
     	"github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
     	"github.com/projectdiscovery/nuclei/v3/pkg/catalog/disk"
     	"github.com/projectdiscovery/nuclei/v3/pkg/loader/workflow"
    +	"github.com/projectdiscovery/nuclei/v3/pkg/output"
     	"github.com/projectdiscovery/nuclei/v3/pkg/progress"
     	"github.com/projectdiscovery/nuclei/v3/pkg/protocols"
    +	"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs"
    +	javascript "github.com/projectdiscovery/nuclei/v3/pkg/protocols/javascript"
     	"github.com/projectdiscovery/nuclei/v3/pkg/templates"
     	"github.com/projectdiscovery/nuclei/v3/pkg/testutils"
     	"github.com/projectdiscovery/ratelimit"
    @@ -73,3 +76,35 @@ func TestCompile(t *testing.T) {
     		}
     	}
     }
    +
    +func TestExecuteWithResultsReturnsArgEvaluationErrorWithoutPanic(t *testing.T) {
    +	options := testutils.DefaultOptions
    +	tmplInfo := &testutils.TemplateInfo{ID: "execute-with-results-arg-evaluation-error"}
    +
    +	testutils.Init(options)
    +
    +	t.Cleanup(func() {
    +		testutils.Cleanup(options)
    +	})
    +
    +	executorOptions := testutils.NewMockExecuterOptions(options, tmplInfo)
    +	executorOptions.JsCompiler = templates.GetJsCompiler()
    +
    +	request := &javascript.Request{
    +		Args: map[string]interface{}{
    +			"token": "{{base64()}}",
    +		},
    +		Code: `module.exports = { success: true, response: "ok" }`,
    +	}
    +	require.NoError(t, request.Compile(executorOptions))
    +
    +	target := contextargs.NewWithInput(context.Background(), "https://example.com:443")
    +
    +	var err error
    +	require.NotPanics(t, func() {
    +		err = request.ExecuteWithResults(target, nil, nil, func(*output.InternalWrappedEvent) {
    +			t.Fatal("unexpected callback on argument evaluation failure")
    +		})
    +	})
    +	require.ErrorContains(t, err, `failed to evaluate expression "base64()"`)
    +}
    
d2217320162d

fix(expressions): only eval template-authored expressions (#7221)

https://github.com/projectdiscovery/nucleiDwi SiswantoMar 16, 2026via ghsa
4 files changed · +105 1
  • cmd/integration-test/http.go+26 0 modified
    @@ -88,6 +88,7 @@ var httpTestcases = []TestCaseInfo{
     	{Path: "protocols/http/multi-request.yaml", TestCase: &httpMultiRequest{}},
     	{Path: "protocols/http/http-matcher-extractor-dy-extractor.yaml", TestCase: &httpMatcherExtractorDynamicExtractor{}},
     	{Path: "protocols/http/multi-http-var-sharing.yaml", TestCase: &httpMultiVarSharing{}},
    +	{Path: "protocols/http/response-data-literal-reuse.yaml", TestCase: &httpResponseDataLiteralReuse{}},
     	{Path: "protocols/http/raw-path-single-slash.yaml", TestCase: &httpRawPathSingleSlash{}},
     	{Path: "protocols/http/raw-unsafe-path-single-slash.yaml", TestCase: &httpRawUnsafePathSingleSlash{}},
     }
    @@ -102,6 +103,31 @@ func (h *httpMultiVarSharing) Execute(filePath string) error {
     	return expectResultsCount(results, 1)
     }
     
    +type httpResponseDataLiteralReuse struct{}
    +
    +func (h *httpResponseDataLiteralReuse) Execute(filePath string) error {
    +	router := httprouter.New()
    +	router.GET("/", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
    +		_, _ = fmt.Fprint(w, `{{md5("Hello")}}`)
    +	})
    +	router.GET("/echo", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
    +		if r.URL.Query().Get("x") != `{{md5("Hello")}}` {
    +			w.WriteHeader(http.StatusBadRequest)
    +			return
    +		}
    +		w.WriteHeader(http.StatusOK)
    +	})
    +	ts := httptest.NewServer(router)
    +	defer ts.Close()
    +
    +	results, err := testutils.RunNucleiTemplateAndGetResults(filePath, ts.URL, debug)
    +	if err != nil {
    +		return err
    +	}
    +
    +	return expectResultsCount(results, 1)
    +}
    +
     type httpMatcherExtractorDynamicExtractor struct{}
     
     func (h *httpMatcherExtractorDynamicExtractor) Execute(filePath string) error {
    
  • integration_tests/protocols/http/response-data-literal-reuse.yaml+34 0 added
    @@ -0,0 +1,34 @@
    +id: response-data-literal-reuse
    +
    +info:
    +  name: Response data literal reuse
    +  author: dwisiswant0
    +  severity: info
    +  description: |
    +    Make sure response-derived {{...}} content stays literal when reused in a
    +    later request.
    +  tags: test,http
    +
    +http:
    +  - raw:
    +      - |
    +        GET / HTTP/1.1
    +        Host: {{Hostname}}
    +
    +    extractors:
    +      - type: regex
    +        name: extracted_body
    +        part: body
    +        regex:
    +          - '(?s)(.*)'
    +        internal: true
    +
    +  - raw:
    +      - |
    +        GET /echo?x={{extracted_body}} HTTP/1.1
    +        Host: {{Hostname}}
    +
    +    matchers:
    +      - type: status
    +        status:
    +          - 200
    
  • pkg/protocols/common/expressions/expressions.go+2 1 modified
    @@ -43,14 +43,15 @@ func EvaluateByte(data []byte, base map[string]interface{}) ([]byte, error) {
     }
     
     func evaluate(data string, base map[string]interface{}) (string, error) {
    +	expressions := FindExpressions(data, marker.ParenthesisOpen, marker.ParenthesisClose, base)
    +
     	// replace simple placeholders (key => value) MarkerOpen + key + MarkerClose and General + key + General to value
     	data = replacer.Replace(data, base)
     
     	// expressions can be:
     	// - simple: containing base values keys (variables)
     	// - complex: containing helper functions [ + variables]
     	// literals like {{2+2}} are not considered expressions
    -	expressions := FindExpressions(data, marker.ParenthesisOpen, marker.ParenthesisClose, base)
     	for _, expression := range expressions {
     		// replace variable placeholders with base values
     		expression = replacer.Replace(expression, base)
    
  • pkg/protocols/common/expressions/expressions_test.go+43 0 modified
    @@ -62,3 +62,46 @@ func TestEval(t *testing.T) {
     		require.Equal(t, item.expected, value, "could not get correct expression")
     	}
     }
    +
    +func TestEvaluateDoesNotReinterpretResolvedValues(t *testing.T) {
    +	items := []struct {
    +		name     string
    +		input    string
    +		expected string
    +		extra    map[string]interface{}
    +	}{
    +		{
    +			name:     "helper syntax in resolved values stays literal",
    +			input:    "/?x={{body}}",
    +			expected: `/?x={{md5("Hello")}}-by-Adelle`,
    +			extra: map[string]interface{}{
    +				"body": `{{md5("Hello")}}-by-Adelle`,
    +			},
    +		},
    +		{
    +			name:     "resolved values cannot access other variables",
    +			input:    "Authorization: {{body}}",
    +			expected: "Authorization: {{secret_token}}",
    +			extra: map[string]interface{}{
    +				"body":         "{{secret_token}}",
    +				"secret_token": "top-secret-cia-mi6-kgb-mossad-classified",
    +			},
    +		},
    +		{
    +			name:     "template-authored placeholders inside helper expressions still resolve",
    +			input:    "{{base64('{{Host}}')}}",
    +			expected: "MTI3LjAuMC4x",
    +			extra: map[string]interface{}{
    +				"Host": "127.0.0.1",
    +			},
    +		},
    +	}
    +
    +	for _, item := range items {
    +		t.Run(item.name, func(t *testing.T) {
    +			value, err := Evaluate(item.input, item.extra)
    +			require.NoError(t, err)
    +			require.Equal(t, item.expected, value)
    +		})
    +	}
    +}
    

Vulnerability mechanics

AI mechanics synthesis has not run for this CVE yet.

References

8

News mentions

0

No linked articles in our index yet.