Files
nuclei/pkg/protocols/http/request_annotations.go
Nakul Bharti 66068dbec2 perf: improve embedded scan runtime efficiency (#7717)
* feat(templates): add execution-local parser lifecycle

* fix(network): include ports and payloads in request count

* fix(headless): include payloads in request count

* feat(core): expose template execution lifecycle

* fix(core): report context-interrupted template executions

* perf(interactsh): allocate caches on first use

* perf(stats): reuse compiled waf detector

* perf(hosterrors): release cache storage on close

* fix(deps): use published tlsx release

* perf(templates): coalesce concurrent parses

* perf(stats): prefilter waf response regexes

* perf(stats): scan waf literals once per response

* fix(sdk): isolate shared interactsh executions

* fix(protocols): preserve interactsh request scope

* fix(interactsh): drain in-flight scoped callbacks

* perf(hosterrors): allocate storage on first failure

* fix(interactsh): recognize raw SNI callbacks

* fix(interactsh): guard non-fuzz callback accounting

* perf(jarm): integrate bounded shared fingerprints

* fix(jarm): honor configured scan proxy

* perf(core): support adaptive template concurrency

* perf(core): enforce shared template concurrency budget

* fix(http): smooth low per-host request limits

* perf(http): remove per-host limiter goroutines

* fix(http): retain host budgets for large embedded scans

* test: consolidate parser and headless coverage

* fix: address PR review comments

* fix(headless): honor scan cancellation
2026-09-15 02:35:21 +05:30

181 lines
6.1 KiB
Go

package http
import (
"context"
"crypto/tls"
"net"
"regexp"
"strings"
"time"
"github.com/projectdiscovery/fastdialer/fastdialer"
commoninteractsh "github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/interactsh"
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/http/httpclientpool"
"github.com/projectdiscovery/nuclei/v3/pkg/types/nucleierr"
"github.com/projectdiscovery/retryablehttp-go"
"github.com/projectdiscovery/utils/errkit"
iputil "github.com/projectdiscovery/utils/ip"
stringsutil "github.com/projectdiscovery/utils/strings"
)
var (
// @Host:target overrides the input target with the annotated one (similar to self-contained requests)
reHostAnnotation = regexp.MustCompile(`(?m)^@Host:\s*(.+)\s*$`)
// @tls-sni:target overrides the input target with the annotated one
// special values:
// request.host: takes the value from the host header
// target: overrides with the specific value
reSniAnnotation = regexp.MustCompile(`(?m)^@tls-sni:\s*(.+)\s*$`)
// @timeout:duration overrides the input timeout with a custom duration
reTimeoutAnnotation = regexp.MustCompile(`(?m)^@timeout:\s*(.+)\s*$`)
// @once sets the request to be executed only once for a specific URL
reOnceAnnotation = regexp.MustCompile(`(?m)^@once\s*$`)
// maxAnnotationTimeout is the maximum timeout allowed for @timeout
// annotations to prevent DoS attacks via extremely large timeout values.
maxAnnotationTimeout = 5 * time.Minute
// ErrTimeoutAnnotationDeadline is the error returned when a specific amount of time was exceeded for a request
// which was allotted using @timeout annotation this usually means that vulnerability was not found
// in rare case it could also happen due to network congestion
// the assigned class is TemplateLogic since this in almost every case means that server is not vulnerable
ErrTimeoutAnnotationDeadline = errkit.New("timeout annotation deadline exceeded").SetKind(nucleierr.ErrTemplateLogic).Build()
// ErrRequestTimeoutDeadline is the error returned when a specific amount of time was exceeded for a request
// this happens when the request execution exceeds allotted time
ErrRequestTimeoutDeadline = errkit.New("request timeout deadline exceeded when notimeout is set").SetKind(errkit.ErrKindDeadline).Build()
)
type flowMark int
const (
Once flowMark = iota
)
// parseFlowAnnotations and override requests flow
func parseFlowAnnotations(rawRequest string) (flowMark, bool) {
var fm flowMark
// parse request for known override annotations
var hasFlowOverride bool
// @once
if reOnceAnnotation.MatchString(rawRequest) {
fm = Once
hasFlowOverride = true
}
return fm, hasFlowOverride
}
type annotationOverrides struct {
request *retryablehttp.Request
cancelFunc context.CancelFunc
interactshURLs []string
err error
}
// parseAnnotations and override requests settings
func (r *Request) parseAnnotations(rawRequest string, request *retryablehttp.Request) (overrides annotationOverrides, modified bool) {
// parse request for known override annotations
// @Host:target
if hosts := reHostAnnotation.FindStringSubmatch(rawRequest); len(hosts) > 0 {
value := strings.TrimSpace(hosts[1])
// handle scheme
switch {
case stringsutil.HasPrefixI(value, "http://"):
request.Scheme = "http"
case stringsutil.HasPrefixI(value, "https://"):
request.Scheme = "https"
}
value = stringsutil.TrimPrefixAny(value, "http://", "https://")
if isHostPort(value) {
request.URL.Host = value
} else {
hostPort := value
port := request.Port()
if port != "" {
hostPort = net.JoinHostPort(hostPort, port)
}
request.URL.Host = hostPort
}
modified = true
}
// @tls-sni:target
if hosts := reSniAnnotation.FindStringSubmatch(rawRequest); len(hosts) > 0 {
value := strings.TrimSpace(hosts[1])
value = stringsutil.TrimPrefixAny(value, "http://", "https://")
var literal bool
switch value {
case "request.host":
value = request.Host
case "interactsh-url":
if r.options == nil || r.options.Interactsh == nil {
overrides.err = commoninteractsh.ErrInteractshClientNotInitialized
overrides.request = request
return overrides, true
}
interactshURL, err := r.options.Interactsh.NewURLWithData("interactsh-url")
if err != nil {
overrides.err = err
overrides.request = request
return overrides, true
}
value = interactshURL
overrides.interactshURLs = append(overrides.interactshURLs, interactshURL)
default:
literal = true
}
ctx := context.WithValue(request.Context(), fastdialer.SniName, value)
request = request.Clone(ctx)
if literal {
request.TLS = &tls.ConnectionState{ServerName: value}
}
modified = true
}
// @timeout:duration
if r.connConfiguration.NoTimeout {
modified = true
var ctx context.Context
if duration := reTimeoutAnnotation.FindStringSubmatch(rawRequest); len(duration) > 0 {
value := strings.TrimSpace(duration[1])
if parsedTimeout, err := time.ParseDuration(value); err == nil {
// Cap at maximum allowed timeout to prevent DoS via extremely large timeout values
if parsedTimeout > maxAnnotationTimeout {
parsedTimeout = maxAnnotationTimeout
}
//nolint:govet // cancelled automatically by withTimeout
// global timeout is overridden by annotation by replacing context
ctx, overrides.cancelFunc = context.WithTimeoutCause(context.TODO(), parsedTimeout, ErrTimeoutAnnotationDeadline)
// add timeout value to context
ctx = context.WithValue(ctx, httpclientpool.WithCustomTimeout{}, httpclientpool.WithCustomTimeout{Timeout: parsedTimeout})
request = request.Clone(ctx)
}
} else {
//nolint:govet // cancelled automatically by withTimeout
// global timeout is overridden by annotation by replacing context
ctx, overrides.cancelFunc = context.WithTimeoutCause(context.TODO(), r.options.Options.GetTimeouts().HttpTimeout, ErrRequestTimeoutDeadline)
request = request.Clone(ctx)
}
}
overrides.request = request
return
}
func isHostPort(value string) bool {
_, port, err := net.SplitHostPort(value)
if err != nil {
return false
}
if !iputil.IsPort(port) {
return false
}
return true
}