fuzz

package
v0.5.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 3, 2026 License: MIT Imports: 23 Imported by: 0

Documentation

Overview

Package fuzz is a low-level, agent-driven fuzzing primitive: inject a caller-supplied payload set into chosen positions of a single HTTP request, send each variant, and report per-payload response signals (status, size, words, lines, time, reflection, baseline delta) with match/filter gating.

It is deliberately NOT a scanner. It makes no decision about *what* to test and emits no findings — the caller (a coding agent, or the vigolium module scanner) brings the intelligence; fuzz provides a fast, scope-aware, controllable send+observe loop. For opinionated, confirmation-backed vulnerability detection use the module scanner (`vigolium scan-request -m ...`) instead; fuzz reports raw signals for the caller to reason over.

The send path, request parsing, and response hashing are reused verbatim from pkg/replay (replay.SendRaw); positions are resolved through pkg/httpmsg insertion points, so the byte-level injection matches what the module scanner does.

Index

Constants

View Source
const (
	AnomalyThresholdLow    = 25
	AnomalyThresholdMedium = 45
	AnomalyThresholdHigh   = 70
)

Anomaly severity thresholds. A single strong signal (an error signature, a server error) clears Medium on its own; weak signals have to stack.

View Source
const DefaultKeyword = "FUZZ"

DefaultKeyword is the literal marker fuzz replaces when present in the raw request.

View Source
const MaxCases = 2_000_000

MaxCases bounds how many sends one invocation may expand to. Clusterbomb is multiplicative — three markers against 500-word lists is 125 million sends — and the whole plan is materialized before the first byte goes out, so without a bound the failure mode is an unexplained OOM rather than a number the operator can react to.

View Source
const MaxMarkers = 8

MaxMarkers bounds how many numbered markers are looked for. Past a handful the combination count under clusterbomb stops being something anyone means to run.

View Source
const MaxReportedAnomalies = 100

MaxReportedAnomalies bounds how many anomalies the report carries, so a pathological run can't return a copy of every response body's worth of metadata.

Variables

This section is empty.

Functions

func BuildAt

func BuildAt(pos Position, raw []byte, payload string) []byte

BuildAt returns the exact bytes that would be sent for one payload at one position, and BuildCase the same for a whole case. Both exist so callers can show an operator what a run will do (--dry-run) without reimplementing the injection rules.

func BuildCase

func BuildCase(c Case, raw []byte) []byte

BuildCase returns the exact bytes one case would send.

func BuiltinNames

func BuiltinNames() []string

BuiltinNames returns the sorted builtin wordlist names for help text.

func LoadPayloads

func LoadPayloads(wordlistRefs, classes, inline []string) ([]string, error)

LoadPayloads assembles the payload set from wordlist references, vulnerability classes, and inline literals, de-duplicated in first-seen order. A wordlist reference is either a builtin name (see BuiltinNames) or a filesystem path; a class is a payloads.Classes() name or alias (e.g. "sqli", "traversal"). Blank lines and lines beginning with '#' in wordlists are skipped.

func NormalizeRawRequest

func NormalizeRawRequest(raw []byte) []byte

NormalizeRawRequest ensures raw ends with a header terminator so net/http.ReadRequest (used by the send path) can parse it. A header-only request whose trailing CRLFs were stripped upstream — e.g. by a line-trimming stdin reader — gets exactly one terminator re-appended; a request that already carries a header/body separator (any request with a body, or an already-well-formed one) is returned unchanged. Idempotent.

func ParseAnomalyThreshold

func ParseAnomalyThreshold(s string) (int, bool)

ParseAnomalyThreshold maps a named level, or a bare number, to a score.

func PayloadClasses

func PayloadClasses() []string

PayloadClasses returns the built-in vulnerability-class names accepted by LoadPayloads' classes argument (and `vigolium fuzz --class`).

Types

type AnomalyConfig

type AnomalyConfig struct {
	// Threshold is the score at or above which a result is reported.
	Threshold int
	// MinPopulation is how many results must be seen before population-based
	// signals (rarity, outliers) are trusted. Below it, only baseline-relative
	// signals contribute — a "rare" status among four samples means nothing.
	MinPopulation int
}

AnomalyConfig controls detection.

func DefaultAnomalyConfig

func DefaultAnomalyConfig() AnomalyConfig

DefaultAnomalyConfig returns detection tuned for a typical run.

type AnomalyReason

type AnomalyReason struct {
	Signal string `json:"signal"`
	Detail string `json:"detail,omitempty"`
	Weight int    `json:"weight"`
}

AnomalyReason is one contributing signal.

type AttackMode

type AttackMode int

AttackMode selects how payloads map onto positions when there is more than one position. The names follow Burp Intruder's, which is the vocabulary operators already have for this.

const (
	// ModeSniper varies one position at a time, leaving the others at their
	// original value. This is the default and the only mode that makes sense
	// for automatically-discovered insertion points: with N params it answers
	// "which param is injectable", not "what combination breaks it".
	ModeSniper AttackMode = iota
	// ModeBatteringRam puts the same payload in every position at once —
	// for when a value has to agree across places (a parameter that also
	// appears in a header, say).
	ModeBatteringRam
	// ModePitchfork advances every position's list in lockstep: the k-th
	// payload of each set together. This is how you test credential pairs,
	// where user[k] must go with password[k] and the cartesian product would
	// be both wrong and enormous.
	ModePitchfork
	// ModeClusterBomb tries every combination. Exhaustive and multiplicative —
	// two 100-item lists are 10,000 sends.
	ModeClusterBomb
)

func ParseAttackMode

func ParseAttackMode(s string) (AttackMode, error)

ParseAttackMode maps a flag value to a mode.

func (AttackMode) String

func (m AttackMode) String() string

String renders the mode as its flag spelling.

type Baseline

type Baseline struct {
	Status int    `json:"status"`
	Length int    `json:"length"`
	Words  int    `json:"words"`
	Lines  int    `json:"lines"`
	Hash   string `json:"content_hash,omitempty"`
	Error  string `json:"error,omitempty"`

	// Timing statistics over Samples sends of the un-fuzzed request. A single
	// sample can't distinguish "this payload made the server work harder" from
	// "the network hiccuped", which is the whole difficulty of time-based
	// blind detection; a mean and a standard deviation can.
	TimeMs      int64   `json:"time_ms"`
	TimeStdDev  float64 `json:"time_stddev_ms,omitempty"`
	Samples     int     `json:"samples,omitempty"`
	TimeSamples []int64 `json:"time_samples,omitempty"`
}

Baseline is the un-fuzzed request's response, used for delta reporting and as the seed for auto-calibration.

type Case

type Case struct {
	Assignments []CaseAssignment
}

Case is one send: every position that gets a payload, and what it gets. Sniper cases carry a single assignment; the multi-position modes carry one per position.

func BuildCases

func BuildCases(mode AttackMode, positions []Position, sets [][]string, fallback []string) ([]Case, error)

BuildCases expands positions and payload sets into the concrete sends a mode implies.

sets[i] belongs to positions[i] for pitchfork and clusterbomb. Sniper and battering ram use one list: sets[0] if present, else the shared fallback.

type CaseAssignment

type CaseAssignment struct {
	Position Position
	Payload  string
}

CaseAssignment places one payload at one position.

type Criteria

type Criteria struct {
	AllStatus bool // --match-status-code all
	Status    NumPredicates
	Sizes     NumPredicates
	Words     NumPredicates
	Lines     NumPredicates
	TimeMs    NumPredicates
	Regex     *regexp.Regexp // matched against the response body
	Headers   []HeaderMatcher
	// TimeZ matches on standard deviations above the baseline mean rather
	// than absolute milliseconds.
	TimeZ NumPredicates
	Mode  MatchMode
}

Criteria is a set of response tests. It backs both the keep set (Matchers) and the drop set (Filters), so the two can never drift in what they can express — a signal you can match on is always a signal you can exclude on.

type Filters

type Filters = Criteria

Filters drop a response when it satisfies the configured categories.

type HeaderMatcher

type HeaderMatcher struct {
	Name  string
	Value *regexp.Regexp
}

HeaderMatcher tests one response header against a regex. An empty Value matches the header's mere presence.

type Job

type Job struct {
	Raw      []byte
	Scheme   string
	Hostname string
	Port     int

	// Positions and Payloads are the simple form: every position gets every
	// payload (sniper). Cases, when set, replaces them with an explicit plan —
	// which is how the multi-position modes express a send that varies several
	// markers at once.
	Positions []Position
	Payloads  []string
	Cases     []Case

	Matchers Matchers
	Filters  Filters
	// BaselineSamples is how many times the un-fuzzed request is sent to build
	// the timing statistics. 1 (the default) skips deviation entirely.
	BaselineSamples int

	// Anomaly, when non-nil, turns on anomaly detection: each response is
	// scored against the baseline and against the run's own population, and
	// (when no explicit Matchers are set) the score replaces "keep everything"
	// as the gate. Detection retains response bodies until the run ends so it
	// can rescore against the finished population.
	Anomaly *AnomalyConfig

	// AutoCalibrate learns the target's wildcard/catch-all response signature
	// from a few improbable probe values and suppresses matching results. This
	// is the primitive's one concession to the catch-all FP problem; it never
	// promotes a result to a finding, only demotes noise.
	AutoCalibrate bool

	Client      *gohttp.Client
	NoRedirects bool
	ExcerptCap  int

	// Sender, when non-nil, replaces the default replay.SendRaw transport for
	// every send (baseline, calibration probes, and each payload variant). It
	// lets the caller route the exact request bytes through an external engine
	// (the Burp bridge) that preserves malformed requests. Must be safe for
	// concurrent use — the engine sends from up to Concurrency goroutines. When
	// set, Client is not required.
	Sender func(context.Context, []byte) *replay.Summary

	Concurrency int
	DelayMs     int

	// OnResult, if set, is called once per send as results complete. It is
	// invoked from multiple goroutines; the callback must be safe to call
	// concurrently (the CLI serializes it behind a mutex).
	OnResult func(Result)

	// OnMatch, if set, is called only for results that pass the matcher/filter
	// gate, with the exact request bytes that were sent — Result carries only
	// signals, so this is how a caller recovers a matched request (e.g. to hand
	// it to Burp's Organizer). Invoked on the same serialized path as OnResult;
	// keep it cheap (append, don't do blocking I/O here).
	OnMatch func(r Result, rawRequest []byte)
}

Job fully describes one fuzz run. Network policy (client, proxy, timeout, redirects) is the caller's — fuzz does not construct clients, mirroring pkg/replay.

type MatchMode

type MatchMode int

MatchMode selects how configured categories combine.

const (
	// MatchAny keeps/drops when at least one category matches (the default,
	// and what ffuf-style tools do).
	MatchAny MatchMode = iota
	// MatchAll requires every configured category to match, which is how you
	// express "a 200 *and* bigger than the baseline" without post-filtering.
	MatchAll
)

type Matchers

type Matchers = Criteria

Matchers keep a response when it satisfies the configured categories. An empty Matchers matches everything — the primitive default, leaning on Filters + auto-calibration to remove noise rather than an opinionated status allowlist.

type NumPredicate

type NumPredicate struct {
	// contains filtered or unexported fields
}

NumPredicate matches a numeric response signal. Exact equality is nearly useless for sizes — a page whose body carries a timestamp or a CSRF token is never byte-identical twice — so the grammar accepts ranges and comparisons alongside it:

500      exactly 500
100-200  inclusive range
>500     greater than
<10      less than
>=500    at least
<=10     at most
!404     anything but

Several may be comma-separated in one flag value; the set matches when any member does.

func (NumPredicate) Match

func (p NumPredicate) Match(v int64) bool

Match reports whether v satisfies the predicate.

func (NumPredicate) String

func (p NumPredicate) String() string

String returns the predicate as written, so reports can echo the operator's own syntax back.

type NumPredicates

type NumPredicates []NumPredicate

NumPredicates is a set combined with OR.

func ParseNumPredicates

func ParseNumPredicates(values []string, flag string) (NumPredicates, error)

ParseNumPredicates parses the comma-separated predicate grammar. Values are also accepted pre-split, so both `--match-size 1-2,5` and `--match-size 1-2 --match-size 5` work.

func ParseNumPredicatesDefaultOp

func ParseNumPredicatesDefaultOp(values []string, defaultOp, flag string) (NumPredicates, error)

ParseNumPredicatesDefaultOp is ParseNumPredicates with an operator applied to bare numbers. `--match-time 500` has always meant "at least 500ms", and turning it into an exact-millisecond match would make it essentially never fire; passing defaultOp=">=" keeps that reading without a second tokenizer at the call site.

func (NumPredicates) Empty

func (ps NumPredicates) Empty() bool

Empty reports whether nothing was configured.

func (NumPredicates) Match

func (ps NumPredicates) Match(v int64) bool

Match reports whether any predicate in the set matches.

func (NumPredicates) String

func (ps NumPredicates) String() string

String renders the set for help and report output.

type Overrides

type Overrides struct {
	Method      string   // replaces the request-line verb
	Headers     []string // "Name: value", added or replaced
	Body        string   // replaces the body, refreshing Content-Length
	QueryAppend string   // appended to the request-line query string (curl's -G)
}

Overrides are curl-style edits applied to the resolved baseline request before positions are worked out. They exist so -X/-H/-d mean the same thing no matter where the request came from: a request pasted with -i is exactly as likely to need an added auth header or a swapped verb as one built from a positional URL, and silently dropping the flags for the -i case makes the tool lie about what it sent.

Because overrides land before insertion-point analysis, a marker placed by an override (-H 'X-Forwarded-For: FUZZ') is discovered like any other.

func (Overrides) Apply

func (o Overrides) Apply(raw []byte) ([]byte, error)

Apply returns raw with the overrides applied.

func (Overrides) Empty

func (o Overrides) Empty() bool

Empty reports whether there is nothing to apply.

type Position

type Position struct {
	// Name identifies the position: the parameter/header/cookie name for
	// insertion points, the keyword for markers, or "method".
	Name string
	// Label is a stable, human/agent-readable position type, e.g.
	// "URL_PARAM", "HEADER", "COOKIE", "MARKER", "METHOD".
	Label string
	// Base is the original value at this position (empty for markers/method).
	Base string
	// contains filtered or unexported fields
}

Position is one place a payload gets injected. Exactly one injection mechanism backs it (insertion point, literal marker, or method rewrite).

func ResolvePositions

func ResolvePositions(raw []byte, sel Selectors) ([]Position, error)

ResolvePositions turns a raw request + selectors into the concrete list of positions to fuzz.

type Report

type Report struct {
	Baseline   Baseline `json:"baseline"`
	Sent       int      `json:"sent"`
	Matched    int      `json:"matched"`
	Calibrated int      `json:"calibrated"`
	Errors     int      `json:"errors"`

	// Anomalies are the results the --anomaly detector flagged, scored
	// against the completed population and sorted most-interesting first.
	// Empty when detection is off.
	Anomalies []Result `json:"anomalies,omitempty"`
	// AnomaliesElided counts anomalies dropped by the report cap.
	AnomaliesElided int `json:"anomalies_elided,omitempty"`
}

Report is the aggregate returned by Run once every send completes.

func Run

func Run(ctx context.Context, job Job) (*Report, error)

Run executes the job: send the baseline, optionally calibrate, then fan out every (position, payload) pair through replay.SendRaw, gating each result and streaming it via job.OnResult. It returns once every send completes.

type Result

type Result struct {
	Position     string `json:"position"`
	PositionType string `json:"position_type"`
	Payload      string `json:"payload"`

	Status      int    `json:"status"`
	Length      int    `json:"length"`
	Words       int    `json:"words"`
	Lines       int    `json:"lines"`
	TimeMs      int64  `json:"time_ms"`
	ContentHash string `json:"content_hash,omitempty"`

	// Reflected is true when the payload appears in the response at all —
	// verbatim or in an escaped/encoded form (a cheap, honest signal, not a
	// vulnerability verdict).
	Reflected bool `json:"reflected"`
	// ReflectedRaw narrows that to a verbatim appearance. The distinction is
	// the whole game for injection classes: a payload echoed back HTML-escaped
	// is the application defending itself, and reporting it identically to an
	// unescaped echo is what buries the real signal.
	ReflectedRaw bool `json:"reflected_raw,omitempty"`
	// ReflectedIn names where it appeared: "body", or "header:Location".
	// Header reflection is invisible to body-only checks yet is exactly the
	// signal behind open redirect and response-splitting.
	ReflectedIn []string `json:"reflected_in,omitempty"`

	// Signals vs the baseline response.
	StatusChanged bool `json:"status_changed"`
	LengthDelta   int  `json:"length_delta"`
	// TimeZ is how many baseline standard deviations this response's time sits
	// above the baseline mean. It is what makes a time-based signal
	// expressible without hand-tuning a millisecond threshold per target:
	// --match-time-z 4 means "slower than this endpoint's own jitter
	// explains". Zero when the baseline wasn't sampled enough to have a
	// deviation.
	TimeZ float64 `json:"time_z,omitempty"`

	// AnomalyScore is the --anomaly detector's score for this response; 0
	// when detection is off. AnomalyReasons explains it.
	AnomalyScore   int             `json:"anomaly_score,omitempty"`
	AnomalyReasons []AnomalyReason `json:"anomaly_reasons,omitempty"`

	// Matched is true when the result passed the matcher/filter gate and was
	// not suppressed by auto-calibration — i.e. the caller asked to see it.
	Matched bool `json:"matched"`
	// Calibrated is true when auto-calibration classified the response as a
	// wildcard/catch-all and suppressed it. Surfaced (not hidden) so the
	// agent can tell "filtered by me" from "looked like the catch-all".
	Calibrated bool `json:"calibrated,omitempty"`

	Error string `json:"error,omitempty"`
}

Result is the outcome of sending one payload at one position. It is the unit streamed as JSONL — each line is self-describing (position + payload), so results need no ordering guarantee under concurrency.

type Selectors

type Selectors struct {
	Mode        string
	NamedPoints []string
	HeaderNames []string
	Keyword     string
}

Selectors describe which positions in a request to fuzz. Resolution order:

  1. a literal Keyword present anywhere in the raw request wins (marker mode);
  2. explicit NamedPoints ("TYPE:name") / HeaderNames;
  3. Mode ("method"|"path"|"params"|"param-name"|"headers"|"cookies"|"all").

An empty Selectors defaults to "auto" = every discovered insertion point.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL