Files
nuclei/pkg/protocols/http/request_annotations.go
dw1 875b229313 fix(http): read annotations only before the request line (#8062)
Raw requests applied @Host, @tls-sni and @timeout from any matching
line of the rendered request, headers and body included. A response
value reused in a later request could carry an "@Host:" line and send
that request, with its body, to a host the template does not name.

The raw parser and the docs already define annotations as the "@"
lines before the request line. raw.SplitAnnotations now holds that
rule once, and the parser, the request annotations, @once and the
compile-time @timeout check all read from it.

Fixes: #8061

Signed-off-by: Dwi Siswanto <[email protected]>
2026-11-06 03:29:42 +07:00

191 lines
6.6 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/protocols/http/raw"
"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
annotations, _ := raw.SplitAnnotations(rawRequest)
// @once
if reOnceAnnotation.MatchString(annotations) {
fm = Once
hasFlowOverride = true
}
return fm, hasFlowOverride
}
// hasTimeoutAnnotation reports whether rawRequest has a @timeout annotation.
func hasTimeoutAnnotation(rawRequest string) bool {
annotations, _ := raw.SplitAnnotations(rawRequest)
return reTimeoutAnnotation.MatchString(annotations)
}
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) {
// rawRequest is rendered, so its headers and body can hold values taken
// from responses. Only the annotation lines may override the request.
annotations, _ := raw.SplitAnnotations(rawRequest)
// @Host:target
if hosts := reHostAnnotation.FindStringSubmatch(annotations); 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(annotations); 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(annotations); 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
}