Skip to content

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

Open
dwisiswant0 wants to merge 2 commits intodevfrom
dwisiswant0/fix/expressions/avoid-helper-eval-in-literal-checks
Open

fix(expressions): avoid helper eval in literal checks#7321
dwisiswant0 wants to merge 2 commits intodevfrom
dwisiswant0/fix/expressions/avoid-helper-eval-in-literal-checks

Conversation

@dwisiswant0
Copy link
Copy Markdown
Member

@dwisiswant0 dwisiswant0 commented Apr 2, 2026

Proposed changes

fix(expressions): avoid helper eval in literal checks

hasLiteralsOnly() currently evaluates helper
expressions while deciding whether "{{...}}"
contains unresolved variables, which makes
validation paths run side-effectful helpers.

Just replace that runtime eval with a
Vars() len check so unresolved-variable
detection literally stays literal (am I writing it
right?), and of course side-effect free.

Also make Evaluate() return template-authored
expression compile/eval errors instead of
logging and then keep continuing, so malformed
helper calls still fail in the rendering path.

Fixes #7320

Proof

$ time ./bin/nuclei -t eval-while-validating.yaml -u "http://localhost:8088/" -var "func=wait_for" -var "args=5" -debug

                     __     _
   ____  __  _______/ /__  (_)
  / __ \/ / / / ___/ / _ \/ /
 / / / / /_/ / /__/ /  __/ /
/_/ /_/\__,_/\___/_/\___/_/   v3.7.1

		projectdiscovery.io

[INF] Current nuclei version: v3.7.1 (unknown) - remove '-duc' flag to enable update checks
[INF] Current nuclei-templates version: v10.4.1 (unknown) - remove '-duc' flag to enable update checks
[WRN] Scan results upload to cloud is disabled.
[INF] New templates added in latest release: 76
[INF] Templates loaded for current scan: 1
[WRN] Loading 1 unsigned templates for scan. Use with caution.
[INF] Targets loaded for current scan: 1
[INF] [eval-while-validating] Dumped HTTP request for http://localhost:8088/?func=wait_for&args=5

GET /?func=wait_for&args=5 HTTP/1.1
Host: localhost:8088
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:72.0) Gecko/20100101 Firefox/72.0
Connection: close
Accept-Encoding: gzip

[DBG] [eval-while-validating] Dumped HTTP response http://localhost:8088/?func=wait_for&args=5

HTTP/1.1 200 OK
Connection: close
Content-Length: 15
Content-Type: text/plain; charset=utf-8
Date: Thu, 02 Apr 2026 02:37:39 GMT

{{wait_for(5)}}
[INF] [eval-while-validating] Dumped HTTP request for http://localhost:8088/reuse

POST /reuse HTTP/1.1
Host: localhost:8088
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:78.0) Gecko/20100101 Firefox/78.0
Connection: close
Content-Length: 20
Accept-Encoding: gzip

data={{wait_for(5)}}
[DBG] [eval-while-validating] Dumped HTTP response http://localhost:8088/reuse

HTTP/1.1 204 No Content
Connection: close
Content-Length: 0
Date: Thu, 02 Apr 2026 02:37:39 GMT

[INF] Scan completed in 3.026557ms. No results found.

real	0m0.816s
user	0m0.472s
sys	0m0.145s

Checklist

  • Pull request is created against the dev branch
  • All checks passed (lint, unit/integration/regression tests etc.) with my changes
  • I have added tests that prove my fix is effective or that my feature works
  • I have added necessary documentation (if appropriate)

Summary by CodeRabbit

  • Bug Fixes

    • Expression evaluation now fails immediately with clear error messages instead of skipping, and avoids leaking resolved secret values.
    • Helper/template-like syntax embedded in resolved inputs is no longer executed.
    • Request argument processing aborts correctly on argument-evaluation errors.
  • Tests

    • Added tests covering expression error handling, helper isolation, unresolved-variable behavior, and argument-evaluation failure handling.

@auto-assign auto-assign bot requested a review from dogancanbakir April 2, 2026 02:38
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 2, 2026

Walkthrough

Expression evaluation now returns errors immediately on compile/eval failures and avoids executing expressions to determine "literals-only." Unresolved-marker replacement is gated by presence of unresolved markers and function tokens. Tests added to ensure helpers are not executed during unresolved-variable checks and to validate error messaging.

Changes

Cohort / File(s) Summary
Evaluation & Replacement
pkg/protocols/common/expressions/expressions.go
Evaluate now fails fast on compile/eval errors (returns wrapped error). Replacement may be set to unresolved-marker only if unresolved markers exist and the compiled expression contains govaluate function tokens; incremental placeholder replacement uses the computed replacement.
Validation (literals-only check)
pkg/protocols/common/expressions/variables.go
hasLiteralsOnly treats nil as literals-only and determines literals-only by checking len(expr.Vars()) == 0 instead of executing the expression.
Tests & Test Helpers
pkg/protocols/common/expressions/expressions_test.go, pkg/protocols/common/expressions/variables_test.go
Added tests covering: helpers not executed during unresolved-variable checks, error messages for invalid template expressions (no leakage of resolved secret values), and behavior with marker-like substrings. Added withTestHelperFunction test helper.
JS Request error path & tests
pkg/protocols/javascript/js.go, pkg/protocols/javascript/js_test.go
Request.getArgsCopy now returns immediately on arg-evaluation error (records failure and increments failed requests). New test ensures ExecuteWithResults returns an evaluation error (no panic) when args contain invalid expression like {{base64()}}.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰
I nibbled templates under moonlight bright,
Kept helpers sleeping, avoided a fright.
Errors call out, secrets tucked tight,
Markers stay markers, functions take flight.
Hooray for tidy burrows tonight! 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix(expressions): avoid helper eval in literal checks' accurately summarizes the main change: preventing helper function evaluation during literal/unresolved-variable detection.
Linked Issues check ✅ Passed All code changes directly address the objectives in #7320: hasLiteralsOnly() now uses Vars() length check instead of evaluation to prevent helper execution, Evaluate() returns errors instead of continuing, and tests validate no helpers are executed during validation.
Out of Scope Changes check ✅ Passed All changes are scoped to the linked issue: modifications to expression evaluation/literal detection in expressions.go/variables.go, comprehensive tests, and related error handling in js.go with supporting tests.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dwisiswant0/fix/expressions/avoid-helper-eval-in-literal-checks

Comment @coderabbitai help to get the list of available commands and usage tips.

@dwisiswant0 dwisiswant0 requested a review from Ice3man543 April 2, 2026 02:39
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pkg/protocols/common/expressions/expressions.go`:
- Around line 57-63: The error messages currently include the post-replacement
value because you call replacer.Replace on expression before
compiling/evaluating; capture the original template (e.g., save the incoming
expression as origExpression) before calling replacer.Replace and use
origExpression in any returned fmt.Errorf messages around
govaluate.NewEvaluableExpressionWithFunctions and evaluation (the compiled/eval
error paths such as where compiled/eval failures are reported) while still
wrapping/preserving the underlying err; keep the replaced expression for the
actual compile/eval calls (use the replaced value for
govaluate.NewEvaluableExpressionWithFunctions and subsequent evaluation) but
ensure errors reference origExpression, not the substituted value.
- Around line 65-70: The Evaluate function currently short-circuits expressions
when any input contains unresolved markers by calling unresolvedVarMarkers(...)
and using replacer.ReplaceOne(...), which rewrites inputs like "{{contact_id}}"
and causes correct expressions (e.g. Evaluate with "{{body != ''}}") to be
mis-evaluated; move this unresolved-marker propagation out of Evaluate and into
the validation path (the same check flow that uses ContainsUnresolvedVariables)
or restrict it only to helper/encoding code paths that can hide markers (e.g.,
base64 helpers), so Update the logic around unresolvedVarMarkers,
replacer.ReplaceOne and the Evaluate method to stop globally rewriting
expression inputs and instead perform this propagation during variable
validation or within specific helper functions; ensure SkipVariablesCheck
handling in pkg/protocols/http/build_request.go no longer receives mutated
values from Evaluate.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1e3975bb-9836-468b-afd4-cd76801b1714

📥 Commits

Reviewing files that changed from the base of the PR and between c6fb1ae and 0734d48.

📒 Files selected for processing (4)
  • pkg/protocols/common/expressions/expressions.go
  • pkg/protocols/common/expressions/expressions_test.go
  • pkg/protocols/common/expressions/variables.go
  • pkg/protocols/common/expressions/variables_test.go

`hasLiteralsOnly()` currently evaluates helper
expressions while deciding whether "{{...}}"
contains unresolved variables, which makes
validation paths run side-effectful helpers.

Just replace that runtime eval with a
`Vars()` len check so unresolved-variable
detection literally stays literal (am I writing it
right?), and of course side-effect free.

Also make `Evaluate()` return template-authored
expression compile/eval errors instead of
logging and then keep continuing, so malformed
helper calls still fail in the rendering path.

Fixes #7320

Signed-off-by: Dwi Siswanto <git@dw1.io>
@dwisiswant0 dwisiswant0 force-pushed the dwisiswant0/fix/expressions/avoid-helper-eval-in-literal-checks branch from 0734d48 to a3f3c99 Compare April 2, 2026 03:30
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
pkg/protocols/common/expressions/variables_test.go (1)

12-25: Note: global map mutation is not parallel-safe.

dsl.HelperFunctions is a plain map[string]govaluate.ExpressionFunction without synchronization. The current implementation works because no t.Parallel() is called, but if parallelism is added later, this would cause data races.

Consider adding a comment or using t.Setenv-style pattern if the test suite ever enables parallel execution.

💡 Optional: Add clarifying comment
 func withTestHelperFunction(t *testing.T, name string, fn govaluate.ExpressionFunction) {
 	t.Helper()
+	// NOTE: dsl.HelperFunctions is not thread-safe; do not use t.Parallel() with this helper
 
 	originalFn, hadFn := dsl.HelperFunctions[name]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/protocols/common/expressions/variables_test.go` around lines 12 - 25, The
helper mutates the global dsl.HelperFunctions map which is not safe for parallel
tests; update withTestHelperFunction to avoid races by either (a) serializing
access with a package-level sync.Mutex around reads/writes to
dsl.HelperFunctions when setting and restoring, or (b) making and assigning a
shallow copy of the map (copy existing entries, set dsl.HelperFunctions =
copiedMap) then restore the original map in the t.Cleanup closure; reference the
function withTestHelperFunction, the global dsl.HelperFunctions, and the
t.Cleanup restore logic when applying the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@pkg/protocols/common/expressions/variables_test.go`:
- Around line 12-25: The helper mutates the global dsl.HelperFunctions map which
is not safe for parallel tests; update withTestHelperFunction to avoid races by
either (a) serializing access with a package-level sync.Mutex around
reads/writes to dsl.HelperFunctions when setting and restoring, or (b) making
and assigning a shallow copy of the map (copy existing entries, set
dsl.HelperFunctions = copiedMap) then restore the original map in the t.Cleanup
closure; reference the function withTestHelperFunction, the global
dsl.HelperFunctions, and the t.Cleanup restore logic when applying the change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 58d91ea6-e30d-42e8-98b6-02bcc1c92185

📥 Commits

Reviewing files that changed from the base of the PR and between 0734d48 and a3f3c99.

📒 Files selected for processing (4)
  • pkg/protocols/common/expressions/expressions.go
  • pkg/protocols/common/expressions/expressions_test.go
  • pkg/protocols/common/expressions/variables.go
  • pkg/protocols/common/expressions/variables_test.go
✅ Files skipped from review due to trivial changes (1)
  • pkg/protocols/common/expressions/variables.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • pkg/protocols/common/expressions/expressions_test.go

`getArgsCopy()` logs and counts argument
evaluation failures, but continues to store Port
in `argsCopy` even when `evaluateArgs()` returns.
This causes malformed JavaScript args like
"{{base64()}}" to panic instead of returning the
original error.

Return the evaluation error immediately after
reporting it to prevent the panic and make sure
proper error propagation.

Signed-off-by: Dwi Siswanto <git@dw1.io>
@neo-by-projectdiscovery-dev
Copy link
Copy Markdown

neo-by-projectdiscovery-dev bot commented Apr 2, 2026

Neo - PR Security Review

No security issues found

Comment @pdneo help for available commands. · Open in Neo

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
pkg/protocols/javascript/js_test.go (1)

90-92: Consider adding a brief comment explaining why JsCompiler setup is needed.

The test setup is correct, but a brief comment would help future maintainers understand that JsCompiler is required for ExecuteWithResults to function, even though this test focuses on argument evaluation errors that occur before JS execution.

📝 Suggested documentation
 	executorOptions := testutils.NewMockExecuterOptions(options, tmplInfo)
+	// JsCompiler is required by ExecuteWithResults even though this test
+	// exercises argument evaluation failure before JS execution occurs
 	executorOptions.JsCompiler = templates.GetJsCompiler()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/protocols/javascript/js_test.go` around lines 90 - 92, Add a one-line
comment near where executorOptions.JsCompiler is set (after the call to
testutils.NewMockExecuterOptions and before ExecuteWithResults is used)
explaining that JsCompiler must be populated for ExecuteWithResults to operate
(even though this test triggers argument evaluation errors prior to JS
execution); reference the JsCompiler field on the returned executorOptions, the
NewMockExecuterOptions helper, and the ExecuteWithResults call so future
maintainers understand why the compiler is initialized in this test.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@pkg/protocols/javascript/js_test.go`:
- Around line 90-92: Add a one-line comment near where
executorOptions.JsCompiler is set (after the call to
testutils.NewMockExecuterOptions and before ExecuteWithResults is used)
explaining that JsCompiler must be populated for ExecuteWithResults to operate
(even though this test triggers argument evaluation errors prior to JS
execution); reference the JsCompiler field on the returned executorOptions, the
NewMockExecuterOptions helper, and the ExecuteWithResults call so future
maintainers understand why the compiler is initialized in this test.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6f794750-32e4-4427-a8f1-c14e66946a55

📥 Commits

Reviewing files that changed from the base of the PR and between a3f3c99 and 64721d8.

📒 Files selected for processing (2)
  • pkg/protocols/javascript/js.go
  • pkg/protocols/javascript/js_test.go

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] validation executes DSL helpers while checking unresolved variables

1 participant