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
- func BuildAt(pos Position, raw []byte, payload string) []byte
- func BuildCase(c Case, raw []byte) []byte
- func BuiltinNames() []string
- func LoadPayloads(wordlistRefs, classes, inline []string) ([]string, error)
- func NormalizeRawRequest(raw []byte) []byte
- func ParseAnomalyThreshold(s string) (int, bool)
- func PayloadClasses() []string
- type AnomalyConfig
- type AnomalyReason
- type AttackMode
- type Baseline
- type Case
- type CaseAssignment
- type Criteria
- type Filters
- type HeaderMatcher
- type Job
- type MatchMode
- type Matchers
- type NumPredicate
- type NumPredicates
- type Overrides
- type Position
- type Report
- type Result
- type Selectors
Constants ¶
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.
const DefaultKeyword = "FUZZ"
DefaultKeyword is the literal marker fuzz replaces when present in the raw request.
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.
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.
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 ¶
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 BuiltinNames ¶
func BuiltinNames() []string
BuiltinNames returns the sorted builtin wordlist names for help text.
func LoadPayloads ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 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.
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).
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.
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 ¶
Selectors describe which positions in a request to fuzz. Resolution order:
- a literal Keyword present anywhere in the raw request wins (marker mode);
- explicit NamedPoints ("TYPE:name") / HeaderNames;
- Mode ("method"|"path"|"params"|"param-name"|"headers"|"cookies"|"all").
An empty Selectors defaults to "auto" = every discovered insertion point.