diffscan

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: 15 Imported by: 0

Documentation

Overview

Package diffscan provides differential analysis tools for detecting injection vulnerabilities through response comparison.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func CountMatches

func CountMatches(response, match []byte) int

func GetCanaryKeys

func GetCanaryKeys(customCanary string) []string

GetCanaryKeys returns canary keys with optional custom canary prepended.

func GetMergedNonMatchingFingerprints

func GetMergedNonMatchingFingerprints(attack1, attack2 *Attack) map[string]bool

GetMergedNonMatchingFingerprints returns the keys whose merged (stable) fingerprint value differs between the two attacks: the merged Fingerprint that survived every confirmation, rather than the transient last sample compared by GetNonMatchingFingerprints. Callers that explain or gate a finding (the report Diff Signal, the body-reflection gate) use this so the reported evidence matches what actually drove the decision.

func GetNonMatchingFingerprints

func GetNonMatchingFingerprints(attack1, attack2 *Attack) map[string]bool

GetNonMatchingFingerprints returns the keys whose last-sample fingerprint value differs between the two attacks. LastFingerprint is the single most recent sample and so includes per-request jitter; callers that need the stable signal the detection fired on should use GetMergedNonMatchingFingerprints.

func Identical

func Identical(candidate *Attack, attack2 *Attack) bool

Identical performs deep comparison of two attack fingerprints. Returns true if fingerprints are exactly equal. Returns false if candidate is nil.

func IsCookieInsertionPoint

func IsCookieInsertionPoint(ip httpmsg.InsertionPoint) bool

IsCookieInsertionPoint checks if insertion point is for cookies

func IsHeaderInsertionPoint

func IsHeaderInsertionPoint(ip httpmsg.InsertionPoint) bool

IsHeaderInsertionPoint checks if insertion point is for headers

func MapInsertionPointToPosition

func MapInsertionPointToPosition(ipType httpmsg.InsertionPointType) string

MapInsertionPointToPosition maps insertion point type to position string

func Similar

func Similar(baselineGroup *Attack, breakAttack *Attack) bool

Similar checks if all key-value pairs in baselineGroup exist and match in breakAttack. This function checks if the second fingerprint is a superset of (or identical to) the first fingerprint. Returns true if the second fingerprint contains all entries from the first.

func SimilarWithTolerance

func SimilarWithTolerance(
	baselineGroup *Attack,
	breakGroup *Attack,
	baselineAttack *Attack,
	breakAttack *Attack,
) bool

SimilarWithTolerance compares consistency between break/baseline groups and individuals. Returns true if similar according to complex conditions. This logic checks the consistency of fingerprint attributes between groups and individual attacks, handling missing keys by referencing the corresponding individual attack.

func VerySimilar

func VerySimilar(attack1 *Attack, attack2 *Attack) bool

VerySimilar checks if two attack fingerprints are "very similar". First, it checks if the number of keys is equal. Then, it iterates through the first fingerprint and checks if each key exists in the second fingerprint with the same value, except for the key "input_reflections" (which allows the value ReflectionCountIncalculable in either). Returns true if very similar (same length and matching key-values, except input_reflections).

Types

type Attack

type Attack struct {
	FirstSnapshot *ResponseSnapshot
	LastSnapshot  *ResponseSnapshot

	LastFingerprint map[string]any

	Payload string
	Anchor  string

	Fingerprint map[string]any

	ResponseFingerprint         *anomaly.Fingerprint
	ResponseKeywordsFingerprint *anomaly.FastResponseKeywords

	ResponseReflections ReflectionCount

	Probe *Probe
	// contains filtered or unexported fields
}

func NewAttack

func NewAttack(quantDiffKeys []string, quantileFactor int, customCanary string) *Attack

func NewAttackFromSnapshot

func NewAttackFromSnapshot(
	snap *ResponseSnapshot,
	probe *Probe,
	payload string,
	anchor string,
	quantDiffKeys []string,
	quantileFactor int,
	customCanary string,
) *Attack

func NewAttackFromSnapshotSimple

func NewAttackFromSnapshotSimple(snap *ResponseSnapshot, anchor string, quantDiffKeys []string, quantileFactor int, customCanary string) *Attack

func (*Attack) AddAttack

func (s *Attack) AddAttack(attack *Attack)

func (*Attack) AllKeysAreQuantitative

func (s *Attack) AllKeysAreQuantitative(keys []string) bool

func (*Attack) Close

func (s *Attack) Close()

func (*Attack) Size

func (s *Attack) Size() int

type Option

type Option struct {
	Confirmations             int
	QuantitativeConfirmations int
	QuantileFactor            int
	QuantitativeDiffKeys      []string
	// CustomCanary is prepended to canaryKeys for response fingerprinting.
	CustomCanary string
}

func NewOption

func NewOption() *Option

type PayloadInjector

type PayloadInjector struct {
	InsertionPoint httpmsg.InsertionPoint

	Options *Option
	// contains filtered or unexported fields
}

func NewPayloadInjector

func NewPayloadInjector(
	baseRequest []byte,
	insertionPoint httpmsg.InsertionPoint,
	httpService *httpmsg.Service,
	httpClient *http.Requester,
	options *Option,
) *PayloadInjector

func (*PayloadInjector) BuildAttack

func (s *PayloadInjector) BuildAttack(payload string, random bool) (*Attack, error)

func (*PayloadInjector) Fuzz

func (s *PayloadInjector) Fuzz(
	baselineAttack *Attack,
	probe *Probe,
) ([]*Attack, error)

func (*PayloadInjector) GetPosition

func (s *PayloadInjector) GetPosition() string

GetPosition returns the position string for this injector's insertion point

func (*PayloadInjector) IsCookie

func (s *PayloadInjector) IsCookie() bool

IsCookie returns true if the insertion point is a cookie parameter

func (*PayloadInjector) IsHeader

func (s *PayloadInjector) IsHeader() bool

IsHeader returns true if the insertion point is a header

func (*PayloadInjector) ProbeAttack

func (s *PayloadInjector) ProbeAttack(payload string) (*Attack, error)

type Probe

type Probe struct {
	Base string
	Name string

	InjectType ProbeInjectType

	RandomAnchor              bool
	UseCacheBuster            bool
	RequireConsistentEvidence bool
	Severity                  int
	// contains filtered or unexported fields
}

func NewProbe

func NewProbe(name string, severity int, breakStrings ...string) *Probe

func (*Probe) AddEscapePair

func (p *Probe) AddEscapePair(args ...string)

func (*Probe) GetAllEscapeSets

func (p *Probe) GetAllEscapeSets() [][]string

func (*Probe) GetBreakStrings

func (p *Probe) GetBreakStrings() []string

func (*Probe) GetEscapeStrings

func (p *Probe) GetEscapeStrings() [][]string

func (*Probe) GetNextBreakPayload

func (p *Probe) GetNextBreakPayload() string

func (*Probe) GetNextEscapePayloadSet

func (p *Probe) GetNextEscapePayloadSet() []string

func (*Probe) SetEscapeStrings

func (p *Probe) SetEscapeStrings(args ...string)

func (*Probe) SetRandomAnchor

func (p *Probe) SetRandomAnchor(randomAnchor bool)

func (*Probe) SetUseCacheBuster

func (p *Probe) SetUseCacheBuster(useCacheBuster bool)

type ProbeInjectType

type ProbeInjectType int
const (
	InjectType_Undefined ProbeInjectType = iota
	InjectType_Append
	InjectType_Prepend
	InjectType_Replace
)

func (ProbeInjectType) String

func (s ProbeInjectType) String() string

type QuantitativeMeasurements

type QuantitativeMeasurements struct {
	Measurements []int64
	Key          string
	Factor       int
}

func NewQuantitativeMeasurements

func NewQuantitativeMeasurements(key string, factor int) *QuantitativeMeasurements

func (*QuantitativeMeasurements) BasicOverlap

func (*QuantitativeMeasurements) Equals

func (qm *QuantitativeMeasurements) Equals(o interface{}) bool

func (*QuantitativeMeasurements) GetQuantileTop

func (qm *QuantitativeMeasurements) GetQuantileTop(list []int64) int64

func (*QuantitativeMeasurements) Merge

func (qm *QuantitativeMeasurements) Merge(newMeasurements *QuantitativeMeasurements)

func (*QuantitativeMeasurements) QuantileOverlap

func (qm *QuantitativeMeasurements) QuantileOverlap(compareMeasurements *QuantitativeMeasurements) bool

func (*QuantitativeMeasurements) String

func (qm *QuantitativeMeasurements) String() string

func (*QuantitativeMeasurements) UpdateWith

func (qm *QuantitativeMeasurements) UpdateWith(val int64)

type ReflectionCount

type ReflectionCount int

ReflectionCount tracks how many times the injected payload appears in the response. Negative values represent special states rather than actual counts.

const (
	ReflectionCountUninitialized ReflectionCount = -1
	ReflectionCountDynamic       ReflectionCount = -2
	ReflectionCountIncalculable  ReflectionCount = -3
)

func (ReflectionCount) String

func (s ReflectionCount) String() string

type ResponseSnapshot

type ResponseSnapshot struct {
	// Filtered response for keyword tracking and anchor reflection counting
	FilteredResponse []byte

	// Full fingerprint for structural comparison (replaces FastResponseVariations + QuantFingerprint)
	Fingerprint *anomaly.Fingerprint

	// WAF detection metadata
	StatusCode int

	// Report generation metadata
	RequestDump   string
	ContentLength int
	URL           string
	Method        string
}

ResponseSnapshot contains extracted data from a ResponseChain. It is designed to be created immediately after an HTTP request, allowing the ResponseChain to be closed and its buffers returned to the pool.

func NewResponseSnapshot

func NewResponseSnapshot(respChain *httputil.ResponseChain) *ResponseSnapshot

NewResponseSnapshot creates a snapshot from a ResponseChain. IMPORTANT: This function closes the ResponseChain after extracting data. The caller should NOT use the ResponseChain after calling this function.

func (*ResponseSnapshot) IsEmptyBody

func (s *ResponseSnapshot) IsEmptyBody() bool

IsEmptyBody reports whether the response carried no body. Error-based, diff-based detection needs the payload to reach something that produces output: when a confirmed response has a 0-length body there is nothing rendered that could have evaluated the payload, so a break-vs-escape difference across empty responses can only be header/cookie jitter, not template evaluation.

func (*ResponseSnapshot) IsNotFound

func (s *ResponseSnapshot) IsNotFound() bool

IsNotFound reports whether the response is a 404. A pair of 404s means the route/template was never reached — in most stacks the router rejects the path before any template layer runs — so a difference between two 404s is not evidence the payload was evaluated. (Reflection into a custom 404 page is still caught separately by the body-reflection gate.)

func (*ResponseSnapshot) IsRedirect

func (s *ResponseSnapshot) IsRedirect() bool

IsRedirect reports whether the response is an HTTP redirect (3xx).

A redirect carries no observable evidence of how the application *processed* an injected payload: the body is a fixed stub and the only payload-dependent part is the echoed Location header. Diff-based, error-based detection (SSTI / behaviour probing) therefore cannot use a redirect as an evaluation signal — a break-vs-escape difference between two redirects is the literal payload being reflected back into the redirect target, not the template engine evaluating it. This was the source of the reported diff-based SSTI false positives on identity/CIAM hosts that 301-redirect every request: break `{{7/0}}` and escape `{{7/1}}` produced identical 301s with identical body length, differing only in the CRC32 of the echoed Location header.

A status *transition* (e.g. 200 → 301) is still a legitimate signal: it shows up in the STATUS_CODE fingerprint attribute, and at least one side of the comparison is not a redirect, so it is never suppressed by this gate.

func (*ResponseSnapshot) IsSuccess

func (s *ResponseSnapshot) IsSuccess() bool

IsSuccess reports whether the response is a 2xx success — i.e. the server actually served a real resource rather than an error/not-found/redirect stub.

Diff-based path-escape and normalization detection compares how the server resolves a "control" path that should reach a genuine resource against an exploit path. When the reached side is a 4xx/5xx (e.g. an empty 404 emitted for everything under a CDN-internal prefix like /cdn-cgi/) or a 3xx, nothing was actually reached: any break-vs-escape difference is header jitter (Ray-ID / ETag / Date / Set-Cookie), not evidence of a path escape. Callers gate on this to require the reached resource be a served 2xx body. (WafBlocked already drops 401/403/429/503; this is the complementary "did we reach a real resource" check that also excludes 404/3xx/5xx.)

func (*ResponseSnapshot) WafBlocked

func (s *ResponseSnapshot) WafBlocked() bool

WafBlocked reports whether the response is a denial / throttle / unavailable page — a WAF/CDN/auth/rate-limit layer rejecting the request — rather than the application actually processing the injected payload. Diff-based detection keys off how the app *reacts* to a payload, so a response that never reached the app is not a vulnerability signal and must not count as a "diff".

Any 401/403/429/503 is treated as blocked regardless of vendor: the previous vendor-gated check (cloudflare/akamai/cloudfront/incapsula only) let a generic app/proxy 403 "Forbidden", 401 auth challenge, or 503 maintenance page slip through as a payload-driven difference — the reported diff-based false positive. A 5xx *application* error such as 500 is deliberately NOT treated as blocked: error-based SSTI/behaviour detection legitimately relies on it.

Jump to

Keyboard shortcuts

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