Nuclei vulnerabilities: Information Disclosure, Denial of Service

Nuclei vulnerabilities: Information Disclosure, Denial of Service

Introduction

Nuclei is a powerful, template-based vulnerability scanner primarily used to identify CVEs and misconfigurations across HTTP services. With over 30k stars on GitHub and a massive community-driven template library, it has become a staple tool for security researchers and DevSecOps teams alike.

At Stoïk, we utilize Nuclei as part of our scanning infrastructure to proactively detect exposures for our insured partners. One of Nuclei’s core strengths is its extensibility: detection logic is defined in YAML templates, which specify the requests, matching conditions, and dynamic variables required to identify a vulnerability.

Finding the bug

The Discovery

While monitoring the stability of our scanning infrastructure, we noticed a Nuclei instance that consistently failed to terminate. Investigation revealed a specific target was triggering a total resource exhaustion: every time Nuclei scanned this HTTP server, CPU usage spiked to 100% and the process hung indefinitely.
To isolate the behavior, we developed a minimal Go-based mock server to replicate the target's HTTP responses. This allowed us to systematically strip down the response body until we identified the trigger.

Root Cause: Template Injection in the Scanner

The "poison" response contained a high density of template tags (e.g., {{example}} or {:variable}). We discovered that Nuclei was inadvertently attempting to process and evaluate template expressions found within the body of the HTTP response it was supposed to be scanning.
While Nuclei is designed to evaluate these tags within its own local YAML templates to craft dynamic requests, it should never treat data from an untrusted remote server as executable template logic. This flaw creates a "Scanner-side Template Injection" (SSTI), allowing a malicious server to force the scanner into executing internal Nuclei functions, leading to:

  • Denial of Service (DoS): Forcing the scanner into infinite loops or heavy computations.
  • Information Disclosure: Potentially leaking internal scanner variables or environment data if the functions are used to echo back internal state.

POC

To demonstrate the flaw, we created a minimal Go-based HTTP server. This server returns a response body containing a Nuclei-specific template function: {{md5("Hello")}}.

If the flaw exists, Nuclei will not treat this as a literal string. Instead, it will evaluate the md5 helper function and use the resulting hash in its subsequent logic.

1. The malicious mock server
The following server listens on port :8000. When scanned, it serves the payload and logs any incoming data to show what the scanner "thinks" it found.

// file main.go (run with `go run main.go`)
// Starts an HTTP server on port :8000 that replies `md5("Hello")`

package main

import (
	"log"
	"io"
	"net/http"
)

func main() {
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		if r.Method == http.MethodPost {
			body, _ := io.ReadAll(r.Body)
			log.Printf("Received POST body: %s", string(body))
			w.Write([]byte("OK"))
			return
		}
		log.Printf("Received request: %s %s", r.Method, r.URL.Path)
		w.Header().Set("Content-Type", "text/html; charset=utf-8")
		w.Write([]byte(`{{md5("Hello")}}`))
	})

	log.Println("Serving on <http://localhost:8000>")
	log.Fatal(http.ListenAndServe(":8000", nil))
}

2. Execution
We run Nuclei against this local server using a standard CVE template. By using the -debug flag, we can observe the internal behavior of the engine.

nuclei -debug -t http/cves/2023/CVE-2023-20073.yaml -u <http://localhost:8000>

3. The results (verification)
Upon execution, our mock server logs the following:

Received POST body: [...]
8b1a9953c4611296a827abf8c47804d7
[...]

Analysis:
The value 8b1a9... is the MD5 hash of the string "Hello". Because this value appears in our server logs, it proves that Nuclei extracted the tag from our HTTP response, executed the md5() function internally, and then passed that processed data back to us in a subsequent request.

Impacted Nuclei templates

To exploit this vulnerability, a template must utilize an extractor to pull data from a server response and subsequently use that data in a following request or condition. This pattern is foundational to Nuclei’s logic, particularly when testing for multi-step vulnerabilities like XSS or CSRF.
Commonly impacted templates include:

Turning Features Into Attack Primitives

While computing an MD5 hash is a harmless proof-of-concept, the actual function surface available to an attacker is vast. Nuclei’s Domain Specific Language (DSL) provides numerous helper functions that can be weaponized by a malicious target:

  • Information Disclosure: Accessing internal environment variables via env_var (if --env-vars is enabled).
  • Denial of Service (Resource Exhaustion): Forcing intensive CPU cycles using regex(), zip(), or cookie_unsign().
  • Process Hanging: Indefinite execution delays using wait_for().
  • SSRF & Internal network probing: Triggering internal network fingerprinting via jarm("internal_ip:port").
  • API Abuse: If run with AI features (-ai), a target can force calls to the OpenAI API using llm_prompt(), potentially exhausting the user's API credits.

In short, in the worst case, a single HTTP response from a server you are scanning can exhaust your CPU, hang your process indefinitely, exfiltrate your environment variables, drain your API credits, and quietly map your internal network — all without you ever noticing. The scanner doesn't just fail; it becomes an unwitting agent of the target it was sent to inspect.

Vulnerability reported to ProjectDiscovery

We believe in responsible disclosure and reached out to ProjectDiscovery to resolve this flaw before public release:

  • January 31, 2026: Initial vulnerability report, PoC, and suggested remediation sent to security@projectdiscovery.io.
  • February 1, 2026: ProjectDiscovery pushed an optimization related to template processing; however, our testing confirmed the core injection vulnerability remained unpatched.
  • February 7, 2026: Follow-up email sent after a week of silence.
  • February 10, 2026: ProjectDiscovery responded, stating they do not consider this a security vulnerability.

Despite our efforts to clarify the risks of Scanner-side Template Injection (specifically regarding DoS and internal network probing) we received no further communication. As the behavior remains present in the current build, users should be aware that scanning untrusted targets may lead to local resource exhaustion or unintended function execution.

Root cause

The vulnerability stems from a "Double Pass" evaluation logic within Nuclei’s expression engine. Specifically, in pkg/protocols/common/expressions/expressions.go, the evaluate function was replacing placeholders (variables) before processing helper functions. This allowed an attacker to inject a function string into a variable that would then be executed in the second pass.

The fix
To resolve this, we propose a logic inversion. By delaying the variable replacement until after all DSL expressions have been evaluated, we ensure that data originating from external variables is treated as a literal string and never parsed as code.

diff --git i/pkg/protocols/common/expressions/expressions.go w/pkg/protocols/common/expressions/expressions.go
index 30a73339..3f70b6d0 100644
--- i/pkg/protocols/common/expressions/expressions.go
+++ w/pkg/protocols/common/expressions/expressions.go
@@ -43,9 +43,6 @@ func EvaluateByte(data []byte, base map[string]interface{}) ([]byte, error) {
 }
 
 func evaluate(data string, base map[string]interface{}) (string, error) {
-	// 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]
@@ -68,6 +65,12 @@ func evaluate(data string, base map[string]interface{}) (string, error) {
 		// replace incrementally
 		data = replacer.ReplaceOne(data, expression, result)
 	}
+
+	// replace simple placeholders (key => value) MarkerOpen + key + MarkerClose and General + key + General to value
+	// We need to replace placeholders after evaluating expressions
+	// to avoid running expression coming from base values (which are untrusted)
+	data = replacer.Replace(data, base)
+
 	return data, nil
 }
 
diff --git i/pkg/protocols/common/expressions/expressions_test.go w/pkg/protocols/common/expressions/expressions_test.go
index ebda9e05..7159a303 100644
--- i/pkg/protocols/common/expressions/expressions_test.go
+++ w/pkg/protocols/common/expressions/expressions_test.go
@@ -34,6 +34,8 @@ func TestEvaluate(t *testing.T) {
 		{input: `_IWP_JSON_PREFIX_{{base64("{\"iwp_action\":\"add_site\",\"params\":{\"username\":\"\"}}")}}`, expected: "_IWP_JSON_PREFIX_eyJpd3BfYWN0aW9uIjoiYWRkX3NpdGUiLCJwYXJhbXMiOnsidXNlcm5hbWUiOiIifX0=", extra: map[string]interface{}{}},
 		{input: "{{}}", expected: "{{}}", extra: map[string]interface{}{}},
 		{input: `"{{hex_encode('PING')}}"`, expected: `"50494e47"`, extra: map[string]interface{}{}},
+		// avoid injection of function from variables
+		{input: `{{body}}`, expected: `{{hex_encode('PING')}}`, extra: map[string]interface{}{"body": `{{hex_encode('PING')}}`}},
 	}
 	for _, item := range items {
 		value, err := Evaluate(item.input, item.extra)

Conclusion

While Nuclei is an invaluable tool for security research, this vulnerability demonstrates that even the tools we use to find bugs can introduce risks. Treating the scanner as an attack surface is essential for maintaining a secure scanning infrastructure.

Recommendations for Nuclei Users
To mitigate the risk of Scanner-side Template Injection (SSTI) and resource exhaustion, we recommend the following security best practices:

  • Isolate Scanning Infrastructure: Run Nuclei in a sandboxed environment (e.g., a restricted Docker container) with limited network egress and strict resource quotas (CPU/Memory limits).
  • Disable Sensitive Features: Avoid using the --env-vars and -ai flags when scanning untrusted or third-party targets, as these significantly expand the impact of function injection.
  • Audit Custom Templates: Carefully review third-party or custom templates that utilize extractors. Be aware that any data captured by these extractors could potentially be re-evaluated by the engine in subsequent requests ;
  • Monitor for Anomalies: Implement monitoring for Nuclei processes that exhibit unusual CPU spikes or fail to terminate within expected timeouts, as these may indicate a targeted DoS attack.
    By treating target responses as untrusted data (even within the context of a vulnerability scanner) security teams can ensure their automation remains a defensive asset rather than a liability.

Indicators of Compromise
Because the victim here is the scanner itself rather than a traditional target, IOCs differ from the usual network or file-based artifacts. The following signals should be treated as suspicious when running Nuclei against untrusted targets.

  • Process & host-based: A Nuclei process stuck at 100% CPU with no termination, a scan duration significantly exceeding the expected baseline, or unexpected outbound connections to api.openai.com from the scanning host.
  • Network-based: POST requests sent back to a scanned target containing computed values (hashes, encoded strings) absent from the original template; outbound JARM fingerprinting connections toward internal IPs outside the scan scope; unexpected internal network probing originating from the scanner host.
  • Log-based: In -debug output, extracted values containing {{ and }} being passed into follow-up requests, or response bodies matching the pattern \{\{[a-zA-Z_]+\(.*\)\}\}. Pay particular attention to the presence of wait_for(, jarm(, env_var(, llm_prompt(, or cookie_unsign( in any server response.

By Martin Desrumaux, Software Engineer @ Stoïk