Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 134 additions & 0 deletions internal/scan/frameworks/detectors/waf.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/

/*

BSD 3-Clause License
(c) 2022-2026 vmfunc, xyzeva & contributors

*/

package detectors

import (
"net/http"

fw "github.com/vmfunc/sif/internal/scan/frameworks"
)

// The detectors here identify the WAF in front of a target. Each keys on a
// header, Set-Cookie name, or block-page string the WAF itself sets, so a page
// that merely names the vendor in prose does not trip it.
func init() {
fw.Register(&cloudflareWAFDetector{})
fw.Register(&sucuriDetector{})
fw.Register(&incapsulaDetector{})
fw.Register(&bigipASMDetector{})
fw.Register(&modSecurityDetector{})
}

// cloudflareWAFDetector detects an active Cloudflare WAF or bot challenge via the
// cf-mitigated header and __cf_chl_ tokens, which appear only on an interception
// response (plain proxying is covered by the cf-ray CDN marker).
type cloudflareWAFDetector struct{}

func (d *cloudflareWAFDetector) Name() string { return "Cloudflare WAF" }

func (d *cloudflareWAFDetector) Signatures() []fw.Signature {
return []fw.Signature{
{Pattern: "cf-mitigated", Weight: 0.6, HeaderOnly: true},
{Pattern: "__cf_chl_", Weight: 0.5},
{Pattern: "Attention Required! | Cloudflare", Weight: 0.4},
}
}

func (d *cloudflareWAFDetector) Detect(body string, headers http.Header) (float32, string) {
base := fw.NewBaseDetector(d.Name(), d.Signatures())
return sigmoidConfidence(base.MatchSignatures(body, headers)), ""
}

// sucuriDetector detects the Sucuri (GoDaddy) cloud WAF via its x-sucuri-id and
// x-sucuri-cache headers, backed by the block-page string.
type sucuriDetector struct{}

func (d *sucuriDetector) Name() string { return "Sucuri WAF" }

func (d *sucuriDetector) Signatures() []fw.Signature {
return []fw.Signature{
{Pattern: "x-sucuri-id", Weight: 0.6, HeaderOnly: true},
{Pattern: "x-sucuri-cache", Weight: 0.4, HeaderOnly: true},
{Pattern: "Sucuri WebSite Firewall", Weight: 0.5},
}
}

func (d *sucuriDetector) Detect(body string, headers http.Header) (float32, string) {
base := fw.NewBaseDetector(d.Name(), d.Signatures())
return sigmoidConfidence(base.MatchSignatures(body, headers)), ""
}

// incapsulaDetector detects the Imperva Incapsula cloud WAF via its x-iinfo
// header, incap_ses_ Set-Cookie name, and block-page incident string. Each is
// Incapsula-exclusive and clears the threshold on its own.
type incapsulaDetector struct{}

func (d *incapsulaDetector) Name() string { return "Imperva Incapsula" }

func (d *incapsulaDetector) Signatures() []fw.Signature {
return []fw.Signature{
{Pattern: "x-iinfo", Weight: 0.6, HeaderOnly: true},
{Pattern: "incap_ses_", Weight: 0.5, HeaderOnly: true},
{Pattern: "Incapsula incident ID", Weight: 0.5},
}
}

func (d *incapsulaDetector) Detect(body string, headers http.Header) (float32, string) {
base := fw.NewBaseDetector(d.Name(), d.Signatures())
return sigmoidConfidence(base.MatchSignatures(body, headers)), ""
}

// bigipASMDetector detects the F5 BIG-IP Application Security Manager via its
// block-page rejection sentence and support-id line. Block-page only, so it never
// fires on a passing response.
type bigipASMDetector struct{}

func (d *bigipASMDetector) Name() string { return "F5 BIG-IP ASM" }

func (d *bigipASMDetector) Signatures() []fw.Signature {
return []fw.Signature{
{Pattern: "The requested URL was rejected. Please consult with your administrator.", Weight: 0.6},
{Pattern: "Your support ID is:", Weight: 0.3},
}
}

func (d *bigipASMDetector) Detect(body string, headers http.Header) (float32, string) {
base := fw.NewBaseDetector(d.Name(), d.Signatures())
return sigmoidConfidence(base.MatchSignatures(body, headers)), ""
}

// modSecurityDetector detects ModSecurity via its generated block page (primary)
// and the Mod_Security/NOYB server-token values (supporting, since suppressible).
type modSecurityDetector struct{}

func (d *modSecurityDetector) Name() string { return "ModSecurity" }

func (d *modSecurityDetector) Signatures() []fw.Signature {
return []fw.Signature{
{Pattern: "This error was generated by Mod_Security", Weight: 0.6},
{Pattern: "Mod_Security", Weight: 0.3, HeaderOnly: true},
{Pattern: "NOYB", Weight: 0.2, HeaderOnly: true},
}
}

func (d *modSecurityDetector) Detect(body string, headers http.Header) (float32, string) {
base := fw.NewBaseDetector(d.Name(), d.Signatures())
return sigmoidConfidence(base.MatchSignatures(body, headers)), ""
}
80 changes: 80 additions & 0 deletions internal/scan/frameworks/detectors/waf_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/

package detectors

import (
"net/http"
"testing"

fw "github.com/vmfunc/sif/internal/scan/frameworks"
)

// TestWAFDetectors_Positive fires each WAF detector on a marker shaped like the
// real header, Set-Cookie, or block-page body that WAF emits.
func TestWAFDetectors_Positive(t *testing.T) {
tests := []struct {
name string
detector fw.Detector
body string
headers http.Header
}{
{"Cloudflare mitigated header", &cloudflareWAFDetector{}, "", http.Header{"Cf-Mitigated": {"challenge"}}},
{"Cloudflare challenge page", &cloudflareWAFDetector{}, `<title>Attention Required! | Cloudflare</title><script>window.__cf_chl_opt={};</script>`, http.Header{}},
{"Sucuri id header", &sucuriDetector{}, "", http.Header{"X-Sucuri-Id": {"17.5"}}},
{"Sucuri block page", &sucuriDetector{}, `<title>Sucuri WebSite Firewall - Access Denied</title>`, http.Header{"X-Sucuri-Cache": {"MISS"}}},
{"Incapsula iinfo header", &incapsulaDetector{}, "", http.Header{"X-Iinfo": {"9-12345-0 0NNN RT(1) q(0) r(0)"}}},
{"Incapsula cookies", &incapsulaDetector{}, "", http.Header{"Set-Cookie": {"visid_incap_123=abc; path=/", "incap_ses_456=def; path=/"}}},
{"Incapsula incident page", &incapsulaDetector{}, `Request unsuccessful. Incapsula incident ID: 123-456`, http.Header{}},
{"F5 BIG-IP ASM block", &bigipASMDetector{}, `<html><body>The requested URL was rejected. Please consult with your administrator.<br>Your support ID is: 987654321</body></html>`, http.Header{}},
{"ModSecurity block page", &modSecurityDetector{}, `<p>This error was generated by Mod_Security.</p>`, http.Header{}},
{"ModSecurity server token", &modSecurityDetector{}, `This error was generated by Mod_Security.`, http.Header{"Server": {"Apache Mod_Security"}}},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
conf, _ := tt.detector.Detect(tt.body, tt.headers)
if conf <= 0.5 {
t.Errorf("%s: confidence = %.3f, want > 0.5", tt.name, conf)
}
})
}
}

// TestWAFDetectors_Negative keeps the detectors quiet on prose that names the
// vendor and on unrelated proxy headers, so a mention or a coincidental header
// cannot trip a WAF finding.
func TestWAFDetectors_Negative(t *testing.T) {
tests := []struct {
name string
detector fw.Detector
body string
headers http.Header
}{
{"Cloudflare plain proxy", &cloudflareWAFDetector{}, "", http.Header{"Cf-Ray": {"8a1-EWR"}}},
{"Cloudflare prose", &cloudflareWAFDetector{}, `<p>We compared Cloudflare and Fastly for WAF coverage.</p>`, http.Header{}},
{"Sucuri prose", &sucuriDetector{}, `<p>Sucuri is a popular website firewall.</p>`, http.Header{}},
{"Incapsula prose", &incapsulaDetector{}, `<p>Imperva Incapsula competes with Cloudflare.</p>`, http.Header{}},
{"F5 unrelated body", &bigipASMDetector{}, `<p>Our support team can help with your F5 BIG-IP setup.</p>`, http.Header{}},
{"ModSecurity prose", &modSecurityDetector{}, `<p>We enabled ModSecurity rules on the origin.</p>`, http.Header{}},
{"plain nginx", &sucuriDetector{}, "", http.Header{"Server": {"nginx/1.25.3"}}},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
conf, _ := tt.detector.Detect(tt.body, tt.headers)
if conf > 0.5 {
t.Errorf("%s: confidence = %.3f, want <= 0.5", tt.name, conf)
}
})
}
}
Loading