nuclei/pkg/templates/templates.go
Nakul Bharti c4fa2c74c1
cache, goroutine and unbounded workers management (#6420)
* Enhance matcher compilation with caching for regex and DSL expressions to improve performance. Update template parsing to conditionally retain raw templates based on size constraints.

* Implement caching for regex and DSL expressions in extractors and matchers to enhance performance. Introduce a buffer pool in raw requests to reduce memory allocations. Update template cache management for improved efficiency.

* feat: improve concurrency to be bound

* refactor: replace fmt.Sprintf with fmt.Fprintf for improved performance in header handling

* feat: add regex matching tests and benchmarks for performance evaluation

* feat: add prefix check in regex extraction to optimize matching process

* feat: implement regex caching mechanism to enhance performance in extractors and matchers, along with tests and benchmarks for validation

* feat: add unit tests for template execution in the core engine, enhancing test coverage and reliability

* feat: enhance error handling in template execution and improve regex caching logic for better performance

* Implement caching for regex and DSL expressions in the cache package, replacing previous sync.Map usage. Add unit tests for cache functionality, including eviction by capacity and retrieval of cached items. Update extractors and matchers to utilize the new cache system for improved performance and memory efficiency.

* Add tests for SetCapacities in cache package to ensure cache behavior on capacity changes

- Implemented TestSetCapacities_NoRebuildOnZero to verify that setting capacities to zero does not clear existing caches.
- Added TestSetCapacities_BeforeFirstUse to confirm that initial cache settings are respected and not overridden by subsequent capacity changes.

* Refactor matchers and update load test generator to use io package

- Removed maxRegexScanBytes constant from match.go.
- Replaced ioutil with io package in load_test.go for NopCloser usage.
- Restored TestValidate_AllowsInlineMultiline in load_test.go to ensure inline validation functionality.

* Add cancellation support in template execution and enhance test coverage

- Updated executeTemplateWithTargets to respect context cancellation.
- Introduced fakeTargetProvider and slowExecuter for testing.
- Added Test_executeTemplateWithTargets_RespectsCancellation to validate cancellation behavior during template execution.
2025-09-15 23:48:02 +05:30

563 lines
23 KiB
Go

//go:generate dstdocgen -path "" -structure Template -output templates_doc.go -package templates
package templates
import (
"io"
"path/filepath"
"strconv"
"strings"
"github.com/projectdiscovery/nuclei/v3/pkg/model"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/code"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/variables"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/dns"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/file"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/headless"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/http"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/javascript"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/network"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/ssl"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/websocket"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/whois"
"github.com/projectdiscovery/nuclei/v3/pkg/templates/types"
"github.com/projectdiscovery/nuclei/v3/pkg/utils"
"github.com/projectdiscovery/nuclei/v3/pkg/utils/json"
"github.com/projectdiscovery/nuclei/v3/pkg/workflows"
"github.com/projectdiscovery/utils/errkit"
fileutil "github.com/projectdiscovery/utils/file"
"go.uber.org/multierr"
"gopkg.in/yaml.v2"
)
// Template is a YAML input file which defines all the requests and
// other metadata for a template.
type Template struct {
// description: |
// ID is the unique id for the template.
//
// #### Good IDs
//
// A good ID uniquely identifies what the requests in the template
// are doing. Let's say you have a template that identifies a git-config
// file on the webservers, a good name would be `git-config-exposure`. Another
// example name is `azure-apps-nxdomain-takeover`.
// examples:
// - name: ID Example
// value: "\"CVE-2021-19520\""
ID string `yaml:"id" json:"id" jsonschema:"title=id of the template,description=The Unique ID for the template,required,example=cve-2021-19520,pattern=^([a-zA-Z0-9]+[-_])*[a-zA-Z0-9]+$"`
// description: |
// Info contains metadata information about the template.
// examples:
// - value: exampleInfoStructure
Info model.Info `yaml:"info" json:"info" jsonschema:"title=info for the template,description=Info contains metadata for the template,required,type=object"`
// description: |
// Flow contains the execution flow for the template.
// examples:
// - flow: |
// for region in regions {
// http(0)
// }
// for vpc in vpcs {
// http(1)
// }
//
Flow string `yaml:"flow,omitempty" json:"flow,omitempty" jsonschema:"title=template execution flow in js,description=Flow contains js code which defines how the template should be executed,type=string,example='flow: http(0) && http(1)'"`
// description: |
// Requests contains the http request to make in the template.
// WARNING: 'requests' will be deprecated and will be removed in a future release. Please use 'http' instead.
// examples:
// - value: exampleNormalHTTPRequest
RequestsHTTP []*http.Request `yaml:"requests,omitempty" json:"requests,omitempty" jsonschema:"title=http requests to make,description=HTTP requests to make for the template,deprecated=true"`
// description: |
// HTTP contains the http request to make in the template.
// examples:
// - value: exampleNormalHTTPRequest
// RequestsWithHTTP is placeholder(internal) only, and should not be used instead use RequestsHTTP
// Deprecated: Use RequestsHTTP instead.
RequestsWithHTTP []*http.Request `yaml:"http,omitempty" json:"http,omitempty" jsonschema:"title=http requests to make,description=HTTP requests to make for the template"`
// description: |
// DNS contains the dns request to make in the template
// examples:
// - value: exampleNormalDNSRequest
RequestsDNS []*dns.Request `yaml:"dns,omitempty" json:"dns,omitempty" jsonschema:"title=dns requests to make,description=DNS requests to make for the template"`
// description: |
// File contains the file request to make in the template
// examples:
// - value: exampleNormalFileRequest
RequestsFile []*file.Request `yaml:"file,omitempty" json:"file,omitempty" jsonschema:"title=file requests to make,description=File requests to make for the template"`
// description: |
// Network contains the network request to make in the template
// WARNING: 'network' will be deprecated and will be removed in a future release. Please use 'tcp' instead.
// examples:
// - value: exampleNormalNetworkRequest
RequestsNetwork []*network.Request `yaml:"network,omitempty" json:"network,omitempty" jsonschema:"title=network requests to make,description=Network requests to make for the template,deprecated=true"`
// description: |
// TCP contains the network request to make in the template
// examples:
// - value: exampleNormalNetworkRequest
// RequestsWithTCP is placeholder(internal) only, and should not be used instead use RequestsNetwork
// Deprecated: Use RequestsNetwork instead.
RequestsWithTCP []*network.Request `yaml:"tcp,omitempty" json:"tcp,omitempty" jsonschema:"title=network(tcp) requests to make,description=Network requests to make for the template"`
// description: |
// Headless contains the headless request to make in the template.
RequestsHeadless []*headless.Request `yaml:"headless,omitempty" json:"headless,omitempty" jsonschema:"title=headless requests to make,description=Headless requests to make for the template"`
// description: |
// SSL contains the SSL request to make in the template.
RequestsSSL []*ssl.Request `yaml:"ssl,omitempty" json:"ssl,omitempty" jsonschema:"title=ssl requests to make,description=SSL requests to make for the template"`
// description: |
// Websocket contains the Websocket request to make in the template.
RequestsWebsocket []*websocket.Request `yaml:"websocket,omitempty" json:"websocket,omitempty" jsonschema:"title=websocket requests to make,description=Websocket requests to make for the template"`
// description: |
// WHOIS contains the WHOIS request to make in the template.
RequestsWHOIS []*whois.Request `yaml:"whois,omitempty" json:"whois,omitempty" jsonschema:"title=whois requests to make,description=WHOIS requests to make for the template"`
// description: |
// Code contains code snippets.
RequestsCode []*code.Request `yaml:"code,omitempty" json:"code,omitempty" jsonschema:"title=code snippets to make,description=Code snippets"`
// description: |
// Javascript contains the javascript request to make in the template.
RequestsJavascript []*javascript.Request `yaml:"javascript,omitempty" json:"javascript,omitempty" jsonschema:"title=javascript requests to make,description=Javascript requests to make for the template"`
// description: |
// Workflows is a yaml based workflow declaration code.
workflows.Workflow `yaml:",inline,omitempty" jsonschema:"title=workflows to run,description=Workflows to run for the template"`
CompiledWorkflow *workflows.Workflow `yaml:"-" json:"-" jsonschema:"-"`
// description: |
// Self Contained marks Requests for the template as self-contained
SelfContained bool `yaml:"self-contained,omitempty" json:"self-contained,omitempty" jsonschema:"title=mark requests as self-contained,description=Mark Requests for the template as self-contained"`
// description: |
// Stop execution once first match is found
StopAtFirstMatch bool `yaml:"stop-at-first-match,omitempty" json:"stop-at-first-match,omitempty" jsonschema:"title=stop at first match,description=Stop at first match for the template"`
// description: |
// Signature is the request signature method
// WARNING: 'signature' will be deprecated and will be removed in a future release. Prefer using 'code' protocol for writing cloud checks
// values:
// - "AWS"
Signature http.SignatureTypeHolder `yaml:"signature,omitempty" json:"signature,omitempty" jsonschema:"title=signature is the http request signature method,description=Signature is the HTTP Request signature Method,enum=AWS,deprecated=true"`
// description: |
// Variables contains any variables for the current request.
Variables variables.Variable `yaml:"variables,omitempty" json:"variables,omitempty" jsonschema:"title=variables for the http request,description=Variables contains any variables for the current request,type=object"`
// description: |
// Constants contains any scalar constant for the current template
Constants map[string]interface{} `yaml:"constants,omitempty" json:"constants,omitempty" jsonschema:"title=constant for the template,description=constants contains any constant for the template,type=object"`
// TotalRequests is the total number of requests for the template.
TotalRequests int `yaml:"-" json:"-"`
// Executer is the actual template executor for running template requests
Executer protocols.Executer `yaml:"-" json:"-"`
Path string `yaml:"-" json:"-"`
// Verified defines if the template signature is digitally verified
Verified bool `yaml:"-" json:"-"`
// TemplateVerifier is identifier verifier used to verify the template (default nuclei-templates have projectdiscovery/nuclei-templates)
TemplateVerifier string `yaml:"-" json:"-"`
// RequestsQueue contains all template requests in order (both protocol & request order)
RequestsQueue []protocols.Request `yaml:"-" json:"-"`
// ImportedFiles contains list of files whose contents are imported after template was compiled
ImportedFiles []string `yaml:"-" json:"-"`
}
// Type returns the type of the template
func (template *Template) Type() types.ProtocolType {
switch {
case len(template.RequestsDNS) > 0:
return types.DNSProtocol
case len(template.RequestsFile) > 0:
return types.FileProtocol
case len(template.RequestsHTTP) > 0:
return types.HTTPProtocol
case len(template.RequestsHeadless) > 0:
return types.HeadlessProtocol
case len(template.RequestsNetwork) > 0:
return types.NetworkProtocol
case len(template.RequestsSSL) > 0:
return types.SSLProtocol
case len(template.RequestsWebsocket) > 0:
return types.WebsocketProtocol
case len(template.RequestsWHOIS) > 0:
return types.WHOISProtocol
case len(template.RequestsCode) > 0:
return types.CodeProtocol
case len(template.RequestsJavascript) > 0:
return types.JavascriptProtocol
case len(template.Workflows) > 0:
return types.WorkflowProtocol
default:
return types.InvalidProtocol
}
}
// IsFuzzing returns true if the template is a fuzzing template
func (template *Template) IsFuzzing() bool {
if len(template.RequestsHTTP) == 0 && len(template.RequestsHeadless) == 0 {
// fuzzing is only supported for http and headless protocols
return false
}
if len(template.RequestsHTTP) > 0 {
for _, request := range template.RequestsHTTP {
if len(request.Fuzzing) > 0 {
return true
}
}
}
if len(template.RequestsHeadless) > 0 {
for _, request := range template.RequestsHeadless {
if len(request.Fuzzing) > 0 {
return true
}
}
}
return false
}
// UsesRequestSignature returns true if the template uses a request signature like AWS
func (template *Template) UsesRequestSignature() bool {
return template.Signature.Value.String() != ""
}
// HasCodeProtocol returns true if the template has a code protocol section
func (template *Template) HasCodeProtocol() bool {
return len(template.RequestsCode) > 0
}
// validateAllRequestIDs check if that protocol already has given id if not
// then is is manually set to proto_index
func (template *Template) validateAllRequestIDs() {
// this is required in multiprotocol and flow where we save response variables
// and all other data in template context if template as two requests in a protocol
// then it is overwritten to avoid this we use proto_index as request ID
if len(template.RequestsCode) > 1 {
for i, req := range template.RequestsCode {
if req.ID == "" {
req.ID = req.Type().String() + "_" + strconv.Itoa(i+1)
}
}
}
if len(template.RequestsDNS) > 1 {
for i, req := range template.RequestsDNS {
if req.ID == "" {
req.ID = req.Type().String() + "_" + strconv.Itoa(i+1)
}
}
}
if len(template.RequestsFile) > 1 {
for i, req := range template.RequestsFile {
if req.ID == "" {
req.ID = req.Type().String() + "_" + strconv.Itoa(i+1)
}
}
}
if len(template.RequestsHTTP) > 1 {
for i, req := range template.RequestsHTTP {
if req.ID == "" {
req.ID = req.Type().String() + "_" + strconv.Itoa(i+1)
}
}
}
if len(template.RequestsHeadless) > 1 {
for i, req := range template.RequestsHeadless {
if req.ID == "" {
req.ID = req.Type().String() + "_" + strconv.Itoa(i+1)
}
}
}
if len(template.RequestsNetwork) > 1 {
for i, req := range template.RequestsNetwork {
if req.ID == "" {
req.ID = req.Type().String() + "_" + strconv.Itoa(i+1)
}
}
}
if len(template.RequestsSSL) > 1 {
for i, req := range template.RequestsSSL {
if req.ID == "" {
req.ID = req.Type().String() + "_" + strconv.Itoa(i+1)
}
}
}
if len(template.RequestsWebsocket) > 1 {
for i, req := range template.RequestsWebsocket {
if req.ID == "" {
req.ID = req.Type().String() + "_" + strconv.Itoa(i+1)
}
}
}
if len(template.RequestsWHOIS) > 1 {
for i, req := range template.RequestsWHOIS {
if req.ID == "" {
req.ID = req.Type().String() + "_" + strconv.Itoa(i+1)
}
}
}
if len(template.RequestsJavascript) > 1 {
for i, req := range template.RequestsJavascript {
if req.ID == "" {
req.ID = req.Type().String() + "_" + strconv.Itoa(i+1)
}
}
}
}
// MarshalYAML forces recursive struct validation during marshal operation
func (template *Template) MarshalYAML() ([]byte, error) {
out, marshalErr := yaml.Marshal(template)
// Use shared validator to avoid rebuilding struct cache for every template marshal
errValidate := tplValidator.Struct(template)
return out, multierr.Append(marshalErr, errValidate)
}
// UnmarshalYAML forces recursive struct validation after unmarshal operation
func (template *Template) UnmarshalYAML(unmarshal func(interface{}) error) error {
type Alias Template
alias := &Alias{}
err := unmarshal(alias)
if err != nil {
return err
}
*template = Template(*alias)
if !ReTemplateID.MatchString(template.ID) {
return errkit.New("template id must match expression %v", ReTemplateID, "tag", "invalid_template")
}
info := template.Info
if utils.IsBlank(info.Name) {
return errkit.New("no template name field provided", "tag", "invalid_template")
}
if info.Authors.IsEmpty() {
return errkit.New("no template author field provided", "tag", "invalid_template")
}
if len(template.RequestsHTTP) > 0 || len(template.RequestsNetwork) > 0 {
_ = deprecatedProtocolNameTemplates.Set(template.ID, true)
}
if len(alias.RequestsHTTP) > 0 && len(alias.RequestsWithHTTP) > 0 {
return errkit.New("use http or requests, both are not supported", "tag", "invalid_template")
}
if len(alias.RequestsNetwork) > 0 && len(alias.RequestsWithTCP) > 0 {
return errkit.New("use tcp or network, both are not supported", "tag", "invalid_template")
}
if len(alias.RequestsWithHTTP) > 0 {
template.RequestsHTTP = alias.RequestsWithHTTP
}
if len(alias.RequestsWithTCP) > 0 {
template.RequestsNetwork = alias.RequestsWithTCP
}
err = tplValidator.Struct(template)
if err != nil {
return err
}
// check if the template contains more than 1 protocol request
// if so preserve the order of the protocols and requests
if template.hasMultipleRequests() {
var tempmap yaml.MapSlice
err = unmarshal(&tempmap)
if err != nil {
return errkit.Wrapf(err, "failed to unmarshal multi protocol template %s", template.ID)
}
arr := []string{}
for _, v := range tempmap {
key, ok := v.Key.(string)
if !ok {
continue
}
arr = append(arr, key)
}
// add protocols to the protocol stack (the idea is to preserve the order of the protocols)
template.addRequestsToQueue(arr...)
}
return nil
}
// ImportFileRefs checks if sensitive fields like `flow` , `source` in code protocol are referencing files
// instead of actual javascript / engine code if so it loads the file contents and replaces the reference
func (template *Template) ImportFileRefs(options *protocols.ExecutorOptions) error {
var errs []error
loadFile := func(source string) (string, bool) {
// load file respecting sandbox
data, err := options.Options.LoadHelperFile(source, options.TemplatePath, options.Catalog)
if err == nil {
defer func() {
_ = data.Close()
}()
bin, err := io.ReadAll(data)
if err == nil {
return string(bin), true
} else {
errs = append(errs, err)
}
} else {
errs = append(errs, err)
}
return "", false
}
// for code protocol requests
for _, request := range template.RequestsCode {
// simple test to check if source is a file or a snippet
if !strings.ContainsRune(request.Source, '\n') && fileutil.FileExists(request.Source) {
if val, ok := loadFile(request.Source); ok {
template.ImportedFiles = append(template.ImportedFiles, request.Source)
request.Source = val
}
}
}
// for javascript protocol code references
for _, request := range template.RequestsJavascript {
// simple test to check if source is a file or a snippet
if !strings.ContainsRune(request.Code, '\n') && fileutil.FileExists(request.Code) {
if val, ok := loadFile(request.Code); ok {
template.ImportedFiles = append(template.ImportedFiles, request.Code)
request.Code = val
}
}
}
// flow code references
if template.Flow != "" {
if len(template.Flow) > 0 && filepath.Ext(template.Flow) == ".js" && fileutil.FileExists(template.Flow) {
if val, ok := loadFile(template.Flow); ok {
template.ImportedFiles = append(template.ImportedFiles, template.Flow)
template.Flow = val
}
}
options.Flow = template.Flow
}
// for multiprotocol requests
// mutually exclusive with flow
if len(template.RequestsQueue) > 0 && template.Flow == "" {
// this is most likely a multiprotocol template
for _, req := range template.RequestsQueue {
if req.Type() == types.CodeProtocol {
request := req.(*code.Request)
// simple test to check if source is a file or a snippet
if !strings.ContainsRune(request.Source, '\n') && fileutil.FileExists(request.Source) {
if val, ok := loadFile(request.Source); ok {
template.ImportedFiles = append(template.ImportedFiles, request.Source)
request.Source = val
}
}
}
}
// for javascript protocol code references
for _, req := range template.RequestsQueue {
if req.Type() == types.JavascriptProtocol {
request := req.(*javascript.Request)
// simple test to check if source is a file or a snippet
if !strings.ContainsRune(request.Code, '\n') && fileutil.FileExists(request.Code) {
if val, ok := loadFile(request.Code); ok {
template.ImportedFiles = append(template.ImportedFiles, request.Code)
request.Code = val
}
}
}
}
}
return multierr.Combine(errs...)
}
// GetFileImports returns a list of files that are imported by the template
func (template *Template) GetFileImports() []string {
return template.ImportedFiles
}
// addRequestsToQueue adds protocol requests to the queue and preserves order of the protocols and requests
func (template *Template) addRequestsToQueue(keys ...string) {
for _, key := range keys {
switch key {
case types.DNSProtocol.String():
template.RequestsQueue = append(template.RequestsQueue, template.convertRequestToProtocolsRequest(template.RequestsDNS)...)
case types.FileProtocol.String():
template.RequestsQueue = append(template.RequestsQueue, template.convertRequestToProtocolsRequest(template.RequestsFile)...)
case types.HTTPProtocol.String():
template.RequestsQueue = append(template.RequestsQueue, template.convertRequestToProtocolsRequest(template.RequestsHTTP)...)
case types.HeadlessProtocol.String():
template.RequestsQueue = append(template.RequestsQueue, template.convertRequestToProtocolsRequest(template.RequestsHeadless)...)
case types.NetworkProtocol.String():
template.RequestsQueue = append(template.RequestsQueue, template.convertRequestToProtocolsRequest(template.RequestsNetwork)...)
case types.SSLProtocol.String():
template.RequestsQueue = append(template.RequestsQueue, template.convertRequestToProtocolsRequest(template.RequestsSSL)...)
case types.WebsocketProtocol.String():
template.RequestsQueue = append(template.RequestsQueue, template.convertRequestToProtocolsRequest(template.RequestsWebsocket)...)
case types.WHOISProtocol.String():
template.RequestsQueue = append(template.RequestsQueue, template.convertRequestToProtocolsRequest(template.RequestsWHOIS)...)
case types.CodeProtocol.String():
template.RequestsQueue = append(template.RequestsQueue, template.convertRequestToProtocolsRequest(template.RequestsCode)...)
case types.JavascriptProtocol.String():
template.RequestsQueue = append(template.RequestsQueue, template.convertRequestToProtocolsRequest(template.RequestsJavascript)...)
// for deprecated protocols
case "requests":
template.RequestsQueue = append(template.RequestsQueue, template.convertRequestToProtocolsRequest(template.RequestsHTTP)...)
case "network":
template.RequestsQueue = append(template.RequestsQueue, template.convertRequestToProtocolsRequest(template.RequestsNetwork)...)
}
}
}
// hasMultipleRequests checks if the template has multiple requests
// if so it preserves the order of the request during compile and execution
func (template *Template) hasMultipleRequests() bool {
counter := len(template.RequestsDNS) + len(template.RequestsFile) +
len(template.RequestsHTTP) + len(template.RequestsHeadless) +
len(template.RequestsNetwork) + len(template.RequestsSSL) +
len(template.RequestsWebsocket) + len(template.RequestsWHOIS) +
len(template.RequestsCode) + len(template.RequestsJavascript)
return counter > 1
}
// MarshalJSON forces recursive struct validation during marshal operation
func (template *Template) MarshalJSON() ([]byte, error) {
type TemplateAlias Template //avoid recursion
out, marshalErr := json.Marshal((*TemplateAlias)(template))
errValidate := tplValidator.Struct(template)
return out, multierr.Append(marshalErr, errValidate)
}
// UnmarshalJSON forces recursive struct validation after unmarshal operation
func (template *Template) UnmarshalJSON(data []byte) error {
type Alias Template
alias := &Alias{}
err := json.Unmarshal(data, alias)
if err != nil {
return err
}
*template = Template(*alias)
err = tplValidator.Struct(template)
if err != nil {
return err
}
// check if the template contains more than 1 protocol request
// if so preserve the order of the protocols and requests
if template.hasMultipleRequests() {
var tempMap map[string]interface{}
err = json.Unmarshal(data, &tempMap)
if err != nil {
return errkit.Wrapf(err, "failed to unmarshal multi protocol template %s", template.ID)
}
arr := []string{}
for k := range tempMap {
arr = append(arr, k)
}
template.addRequestsToQueue(arr...)
}
return nil
}
// HasFileProtocol returns true if the template has a file protocol section
func (template *Template) HasFileProtocol() bool {
return len(template.RequestsFile) > 0
}